Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion deployments/injective_mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"treasuryNote": "⛔ NULL, and script 02 reverts on it - readAddress refuses a null - so this must be filled BETWEEN script 01 and script 02. It cannot be pre-filled because it IS the Safe, and the Safe's address is not known until script 01 has created it. ⇒ After script 01, copy governance.safe into this field. That is what testnet does (choice.treasury == governance.safe 0x0ee0Db41...), and it is right for the same reason: protocol revenue should land somewhere the signers already control rather than at a fresh address nobody has tested a withdrawal from. ☑ CONFIRMED 2026-09-08 after the alternative was examined and REJECTED. Choice v1's Treasury Multisig inj1c2yleauy9say73tsx3dk5tvlgwwzdh96r76zv4 was proposed and ruled out on two independent grounds, both read off the chain that day. (1) It has NEVER ACTED: zero proposals created in its lifetime, one of its two voters has no account on chain at all and the other has never sent a transaction on either side. (2) 🔴 THE ONE THAT BITES EVEN IF THAT WERE FIXED: it is a cw3 CosmWasm contract with ZERO EVM code, and `harvest` pays the treasury with an ERC20 `transfer`. For a PURE EVM ERC20 - and mainnet USDC 0xa00C59fF... is exactly that - the balance lands in EVM state at an address whose only occupant is a CosmWasm contract, and a CosmWasm contract cannot make EVM calls. Those tokens would be unreachable by anybody, for ever, no matter who holds the voter keys. Only bank-backed denoms like wINJ would even be theoretically recoverable. A Safe is an EVM contract and has neither problem. 🔑 A PREVIOUS VERSION OF THIS NOTE SAID getting the treasury wrong means \"a Tier-1 redeploy of both rather than a setting change\". THAT IS WRONG and it mattered, because it made this decision look irreversible: `ChoiceFeeController.setTreasury` is `onlyOwner`, so the timelock can move it at any time. It is a constructor argument only in the sense that it must be SET at construction. ✅ Low stakes at launch on top of that: with protocolFeeSplitRatio at 0 nothing is ever collected, so this address receives nothing until the fee is turned on.",
"directTransferBurnSink": "0xD51CF52D8F11a7c6AB65c538aB5bB483374fAc33",
"exchangeSubaccountBurnSink": "0x460F24d98f2D53d34772BAE1880FEEb6c0950965",
"buybackBurnSink": null,
"buybackBurnSink": "0x65Dc46Ee554A27bC790710f9fAee74B427c1C57D",
"buybackBurnSinkNote": "\ud83d\udd34 RESERVED, NOT DEPLOYED. This is a PREDICTION - Create3Factory.computeAddress(keccak256(\"CHOICE-V2/BuybackBurnSink/1.5.0\")) against factory 0xa4753315E17b79A6f0Dc7739650D019dDD807Df5, read off 1776 on 2026-09-13, with eth_getCode answering 0x at the time. It is filled BEFORE the contract exists on purpose, to break the sink circularity: PositionLocker's constructor takes this address as launchpadTreasury and script 05 reads it here (readAddress, and deliberately NOT requireCode), the sink's BURN_TOKEN is an immutable SPROUT address, and SPROUT is a launch on the new core which needs the locker. Writing the prediction first is what cuts that loop. \u26a0\ufe0f WHY IT STAYS CODELESS FOR WEEKS, which testnet never did: the SPROUT launch is deliberately held back (Dan, 2026-09-13), so the pad goes live and graduates onto Choice v2 with no burn token in existence. Revenue accrues to THIS ADDRESS in the meantime - ERC20 balances live in the token's storage, so the sink inherits every wei the moment script 09 lands it here, and script 09 already asserts deployed == prediction. \ud83d\udd34 That is also why E5 stops being cleanup: Create3.addressOf(salt) is NOT namespaced by msg.sender and two hot keys are still whitelisted on the factory, so until the lockdown either key can place arbitrary code at this salt and take whatever has accrued. Lock the factory down after script 05 and pay one timelock cycle to re-whitelist for 09/10. \u26a0\ufe0f It also PINS the sink to 1.5.0 - a 1.6.0 is a different salt and a different address, and anything already parked here would have to be swept out of 1.5.0 after deploying it anyway. Script 08 SKIPS a codeless entry, so this does not turn the ownership verifier red.",
"launchFeeCranker": null,
"protocolFeeSplitRatio": 0,
"defaultProtocolFeeForDynamicFeePool": 0,
Expand Down
13 changes: 12 additions & 1 deletion script/08_VerifyOwnership.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -638,11 +638,22 @@ contract VerifyOwnership is BaseScript {
/// somebody still has to take, not a pass.
function _requireOwnedBy(string memory key, address expected, string memory expectedName) internal {
address at = readAddressOrZero(key);
if (at == address(0) || at.code.length == 0) {
if (at == address(0)) {
skipped++;
console.log(string.concat(" [skip] ", key, " is not in the book yet"));
return;
}
// 🔑 In the book but codeless is a DIFFERENT state, and saying "not in the book yet"
// about it invites somebody to helpfully fill in an address that is already there.
// A RESERVED entry is deliberate: `choice.buybackBurnSink` is written as a CREATE3
// prediction before the contract exists, to break the sink circularity (locker needs
// the sink, sink needs SPROUT, SPROUT needs the locker). With the SPROUT launch held
// back this state now lasts weeks rather than minutes, so it has to read correctly.
if (at.code.length == 0) {
skipped++;
console.log(string.concat(" [skip] ", key, " is RESERVED (no code yet) at ", vm.toString(at)));
return;
}
checked++;

(bool hasOwner, address current) = _owner(at);
Expand Down
Loading