fix(rewards): resolve the reward manager from chain state, not the config secret - #1015
Merged
Merged
Conversation
…nfig secret Launchpad reward-code creation derived rewards_manager_pubkey from the currently configured launchpad deterministic secret. The Solana reward manager account is created once, at coin launch, with whatever secret was live then, and it can never move — so once that secret is rotated, an already-launched mint derives a reward manager that has no Solana account. The pool-creation branch then made it worse. Its comment reads "first reward against this mint? create the pool", but the condition it actually tests is "no pool exists for this DERIVED reward manager". Those coincide only while the secret never changes. After a rotation an established mint looks brand-new, and the code silently creates a parallel pool bound to a reward manager that does not exist on chain. Resolve it instead from sol_reward_manager_inits, which the Solana indexer writes from observed InitRewardManager instructions. That is ground truth and is unaffected by the secret rotating. It is also the source redemption already reads, which is why redemption is unaffected while creation was not. Creating a pool still needs the reward manager PRIVATE key, which only derivation can produce, so the creation path derives the keypair and checks its public half against the reward manager Solana actually has. A mismatch is exactly the rotated-mint case and now fails loudly instead of creating a pool nothing can redeem against. In practice the check rarely fires: resolving the real reward manager means an established mint finds its existing pool and never enters the creation branch at all, while a mint launched under the current secret derives the matching key and proceeds as before. Shared by both call sites — the HTTP handler and the bulk CLI duplicated this logic — so they cannot drift. Tests cover the rotated-mint case (no pool created, loud error), an unindexed mint, and a current-secret mint still creating its pool. Verified red against the previous behavior: the rotated-mint cases fail with "an established mint must never look brand-new". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Launchpad reward-code creation derived
rewards_manager_pubkeyfrom the currently configured launchpad deterministic secret:The Solana reward manager account is created once, at coin launch, using whatever secret was live at the time — and it can never move. So if that secret is later rotated, an already-launched mint starts deriving a reward manager that has no Solana account.
The pool-creation branch then turns that into durable bad state. Its comment says "first reward against this mint? create the pool", but the condition it actually tests is "no pool exists for this derived reward manager". Those mean the same thing only while the secret never changes. After a rotation an established mint looks brand-new, and the code quietly creates a parallel pool bound to a reward manager that doesn't exist on chain.
Rewards written to that pool are not lost — redemption resolves the reward manager by mint, independently — but core then records a reward manager that doesn't correspond to the mint's actual Solana account.
The fix
Resolve it from
sol_reward_manager_inits, which the Solana indexer writes from observedInitRewardManagerinstructions. That's ground truth, and it's unaffected by the secret rotating. It's also the sourcev1_coins_post_redeem.goalready reads — which is why redemption was unaffected while creation wasn't.Creating a pool still needs the reward manager private key for
rm_owner_signature, and only derivation produces that. So the creation path derives the keypair and checks its public half against the reward manager Solana actually has. A mismatch is precisely the rotated-mint case, and it now fails loudly instead of creating a pool nothing can redeem against.In practice that check rarely fires, which is the point:
Both call sites (the HTTP handler and the bulk CLI) duplicated this logic; they now share
launchpad.PrepareRewardPoolso they can't drift.Tests
launchpad/reward_pool_test.gocovers:ErrRewardManagerMismatchand creates nothingErrRewardManagerNotIndexedwithout touching cometbftVerified red against the previous behavior: reverting to derive-then-create fails three of these, including with
an established mint must never look brand-new; creating a pool here is the phantom-pool bug.Notes for review
app.poolrather thanapp.writePool— the latter is nil whenWriteDbUrlis unset, and this is the same table and pool the redeem path already reads.🤖 Generated with Claude Code