Repository navigation
Init user delegate - #44
Closed
Brennan Lamey (brennanjl) wants to merge 7 commits into
Closed
Brennan Lamey (brennanjl) wants to merge 7 commits into
Brennan Lamey (brennanjl) wants to merge 7 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Anchor deserializes Account<T> before applying realloc constraints. On mainnet, the existing BridgeCardsState account is 41 bytes (no `migrated` field), so deploying the new binary would cause every instruction — including set_migrated itself — to fail with a Borsh UnexpectedEof, with no on-chain recovery path. Fix: change the state account to UncheckedAccount so realloc can happen before Borsh deserialization. Seeds, owner, discriminator, bump, and admin are all validated manually in the handler after expansion. Adds set_migrated_tests.rs covering the basic cases and an explicit upgrade-path test that shrinks the account to the old 41-byte layout and verifies set_migrated can repair it. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
The old approach added `migrated: bool` to BridgeCardsState, which grew
the account from 41 to 42 bytes. This required a realloc on first call
to set_migrated, and deploying the new binary would break all callers
immediately (Borsh UnexpectedEof) with no recovery path in the same tx.
New approach: a separate MigrationState PDA at seeds [b"migration"].
- When the PDA exists (owned by bridge-cards): program is migrated.
- When it doesn't: program operates normally.
- set_migrated(true) → create_account CPI
- set_migrated(false) → lamports zeroed, owner reassigned to system program
BridgeCardsState stays at 41 bytes. The migration binary deploys and
works against all existing mainnet accounts immediately. Zero downtime
with a single upgrade — no two-phase deploy, no atomic batch needed.
Deployment order:
1. Deploy updated Monorail (passes migration_state PDA to affected
instructions — old binary ignores extra accounts, no failures).
2. Deploy migration binary. New binary validates migration_state.
All 52 tests pass.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
…er_delegate Both instructions loaded BridgeCardsState only for the migrated check, which was replaced with the MigrationState PDA approach. Neither instruction uses the state account for any other constraint or logic, so it is redundant. Removing it reduces the interface change (callers only need to pass migration_state, not state + migration_state). Addresses reviewer comment on PR #40. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…mpatibility For a zero-downtime deploy, Monorail is updated first to send the extra migration_state account, and the new binary is deployed afterward. This only works if migration_state is at the END of each instruction's account list — extra accounts go to remaining_accounts in the old binary without shifting existing account positions. Addresses reviewer ordering concern on PR #40. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Allows new users to be onboarded onto legacy merchants without a separate add_or_update_user_delegate call. The caller (spender debitor) pre-approves the deterministic PDA address as their SPL delegate; the first call to this instruction creates the UserDelegateState in the same transaction. New accounts are zero-initialized (zero transfer limits) which is safe on the CPI path — only debit_user checks limits, and that instruction is blocked post-migration. The spender program's destination whitelist and pause flag are the operative controls here. Adds `debitor` (mutable signer, payer for account creation) and `system_program` to the account list. Account ordering: caller_proof, user_delegate_account, debitor, user_token_account, destination_token_account, mint, token_program, system_program. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Brennan Lamey (brennanjl)
changed the base branch from
main
to
brennan/pulls-migration
September 30, 2026 17:40
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.
We have this migration system to facilitate moving from one contract to another. Something I want to maintain is, for legacy card programs (called merchants in the bridge-cards contract), new users can still onboard using the legacy approval flow. In order to do this, I think we need init_if_needed.
Can you see if this is safe to add?
What this does
Adds
init_if_neededtouser_delegate_accountincpi_transferso that new users can be onboarded onto legacy merchants without a separateadd_or_update_user_delegatecall.New user onboarding flow:
UserDelegateStatePDA address offline — seeds are[USER_DELEGATE_SEED, merchant_id, mint, user_ata], computable without any on-chain interaction.Approveto set that PDA as their token account delegate (SPL doesn't require the account to exist yet).transfer_using_legacy_delegatein the spender program, this instruction creates theUserDelegateStateviainit_if_neededin the same transaction, then executes the SPL transfer.Safety analysis
caller_proof/ SpenderState PDA) can call this instruction. The spender's debitor initiates and pays for account creation.cpi_transferusesUserDelegateStateonly as an SPL signing authority; it never readsper_transfer_limitor period limits. Those are only checked bydebit_user, which is blocked post-migration.init_if_neededchecks the discriminator; an existing account won't be overwritten. PDA seeds also prevent account substitution.init_if_needed.set_migrated(true), a zero-limitUserDelegateStateis created. This would blockdebit_userfor that user untiladd_or_update_user_delegatesets proper limits. Acceptable since the merchant manager can fix it, and post-migrationdebit_useris blocked anyway.Changes
user_delegate_account→init_if_neededwithpayer = debitordebitor: Signer<'info>(mutable — payer for account creation)system_program: Program<'info, System>user_delegate_account.bumpto handle newly-created accounts🤖 Generated with Claude Code