Skip to content

mainnet: reserve the sink address before it exists (P1) - #54

Merged
danvaneijck merged 1 commit into
mainfrom
feat/reserve-mainnet-sink-address
Sep 13, 2026
Merged

danvaneijck merged 1 commit into
mainfrom
feat/reserve-mainnet-sink-address

Conversation

@danvaneijck

Copy link
Copy Markdown
Member

P1 from the sprout.fun mainnet cutover plan. No transactions — this is a book entry and a log-line fix.

The circularity this cuts

PositionLocker constructor  needs  choice.buybackBurnSink   (readAddress REVERTS on null)
BuybackBurnSink constructor needs  SPROUT as immutable BURN_TOKEN
SPROUT                      is     a LAUNCH on the new core
the new core                needs  the locker (via the settler) to graduate anything

Testnet never hit it — its sink already existed, because SPROUT was launch 2 on the previous core. A fresh mainnet deployment hits it head on.

Broken by predicting the sink. computeAddress(salt) is a pure function of the salt, so the address can be written into the book before the contract exists, and script 09 already asserts deployed == sink when it finally lands there — the reservation is self-checking.

BuybackBurnSink/1.5.0 -> 0x65Dc46Ee554A27bC790710f9fAee74B427c1C57D

Read off 1776 against factory 0xa4753315... on 2026-09-13, with eth_getCode answering 0x at the time. The other four launchpad salts (settler 1.4.0, locker 1.3.0, guard hook 1.0.0, cranker 2.1.0) were checked unclaimed in the same pass and are deliberately not written — scripts 05 and 10 write those themselves, and a pre-filled entry would be adopted rather than deployed.

What is new, and why the note is long

The SPROUT launch is being held back several weeks (Dan, 2026-09-13), so the pad goes live and graduates onto Choice v2 with no burn token in existence. That means this address stays codeless while the pad is live and taking fees — and it is also where those fees go: the pad core's FEE_TREASURY and the locker's launchpadTreasury both point here.

That is deliberate and it is the good outcome: ERC20 balances live in the token's storage, so the sink inherits every wei the moment script 09 lands at this address. The burn becomes retroactive with no manual transfer and no promise.

🔴 It does promote the CREATE3 factory lockdown from cleanup to a real surface. Create3.addressOf(salt) is not namespaced by msg.sender and two hot keys stay whitelisted, so until the lockdown either key can place arbitrary code at this salt and take whatever has accrued. Lock down after script 05; one timelock cycle re-whitelists for 09/10.

⚠️ It also pins the sink to 1.5.0 — a 1.6.0 is a different salt and a different address.

Script 08

_requireOwnedBy already skipped a codeless entry, correctly, but printed "is not in the book yet" — the message for an absent entry. This one is present and deliberate, and over a weeks-long reservation that wording is an invitation to helpfully fill in an address that is already there. Reserved and absent now read differently, and the reserved line prints the address so it can be checked against the prediction.

Checks

  • forge build clean; forge fmt --check clean on the changed script.
  • Book parses; _requireOwnedBy still skips (does not flag) a codeless entry, so the ownership verifier stays green.

🤖 Generated with Claude Code

Breaks the sink circularity for the mainnet deploy. PositionLocker's
constructor takes launchpadTreasury and script 05 reads it from
choice.buybackBurnSink (readAddress, deliberately not requireCode); the sink's
BURN_TOKEN is an immutable SPROUT address; SPROUT is a launch on the new core,
which needs the locker to graduate anything. Writing the CREATE3 prediction
first is what cuts the loop, and script 09 already asserts deployed ==
prediction, so the reservation is self-checking when it is finally taken.

  BuybackBurnSink/1.5.0 -> 0x65Dc46Ee554A27bC790710f9fAee74B427c1C57D

computeAddress against factory 0xa4753315... read off 1776 on 2026-09-13, with
eth_getCode answering 0x at the time. The other four launchpad salts (settler
1.4.0, locker 1.3.0, guard hook 1.0.0, cranker 2.1.0) were checked unclaimed in
the same pass and are NOT written here: script 05 and script 10 write those
themselves, and a pre-filled entry would be adopted rather than deployed.

The new thing this has to survive, which testnet never did: the SPROUT launch
is being held back several weeks, so this address stays codeless while the pad
is live and taking fees. It is also where those fees go - the pad core's
FEE_TREASURY and the locker's launchpadTreasury both point here - because ERC20
balances live in the token's storage, so the sink inherits every wei the moment
script 09 lands it. That makes the burn retroactive with no manual transfer and
no promise, which is the whole point of the sink.

Script 08 stopped calling that state "not in the book yet". A codeless entry
was already skipped, correctly, but the message it printed was for an ABSENT
entry and this one is present and deliberate - an invitation to helpfully fill
in an address that is already there. Reserved and absent now read differently,
and the reserved line prints the address so it can be checked against the
prediction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@danvaneijck
danvaneijck merged commit 9f09011 into main Sep 13, 2026
2 checks passed
@danvaneijck
danvaneijck deleted the feat/reserve-mainnet-sink-address branch September 13, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant