Skip to content

add support for migration to spender program - #40

Draft
Brennan Lamey (brennanjl) wants to merge 8 commits into
mainfrom
brennan/pulls-migration
Draft

Brennan Lamey (brennanjl) wants to merge 8 commits into
mainfrom
brennan/pulls-migration

Conversation

@brennanjl

Copy link
Copy Markdown

No description provided.

Brennan Lamey (brennanjl) added a commit that referenced this pull request Sep 10, 2026
…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
Brennan Lamey (brennanjl) added a commit that referenced this pull request Sep 10, 2026
…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
Adds three new instructions to support the gradual migration from the
bridge-cards program to the spender-program-solana:

cpi_transfer:
  CPI target callable only by the spender program (verified via the
  SpenderState PDA as caller_proof). The spender validates destination
  allowlists; bridge-cards executes the SPL transfer using the existing
  UserDelegate PDA as signing authority. Intentionally skips the
  per-transfer and period limits enforced by debit_user — those controls
  are managed upstream by Monorail before a debit is authorized.

set_migrated:
  Creates or closes a MigrationState PDA (seeds = [b"migration"]).
  When the PDA exists and is owned by bridge-cards, all instructions
  except cpi_transfer reject with ProgramMigrated. BridgeCardsState
  is unchanged (stays 41 bytes) so deploying this binary is safe against
  all existing mainnet accounts with zero downtime and no account resize.

MigrationState PDA is placed last in every instruction's account list
so old callers (not yet updated to pass it) continue to work — the
extra account silently goes to remaining_accounts in the old binary.
Gradual rollout: update Monorail first, deploy binary second.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
…entional cpi_transfer design

- realloc before assign in set_migrated(false) so bridge-cards still owns
  the account when the BPF runtime validates the resize at finalization
- emit MigrationStateUpdated only when the PDA is actually created/closed,
  not on no-op idempotent calls
- document in cpi_transfer that velocity limits and destination whitelist
  validation are intentionally delegated to the spender program

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
The previous guard (lamports > 0) would enter the close path for a
dusted-but-never-created PDA, then fail at runtime when trying to debit
lamports from a system-owned account. Symmetric with set_migrated(true)
which correctly uses owner != &ID.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
New users can be onboarded onto legacy merchants without a separate
add_or_update_user_delegate call. They pre-approve the deterministic
UserDelegateState PDA address as their SPL delegate; the first call to
transfer_using_legacy_delegate creates the account via init_if_needed in
the same transaction.

New accounts are zero-initialized (zero transfer limits), which is safe —
cpi_transfer uses UserDelegateState only as an SPL signing authority and
never reads limits. The spender program's destination whitelist and pause
flag are the operative controls on this path.

Adds debitor (mutable signer, payer) and system_program to the account list.
New 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
- Remove seeds::program = ID from init_if_needed constraint on
  user_delegate_account; this was preventing Anchor from generating the
  Bumps accessor for ctx.bumps.user_delegate_account. The default seeds
  scope is already the current program, so the constraint was redundant.
- cargo fmt: fix pre-existing formatting issues across close_account.rs,
  set_migrated.rs, debit_user_tests.rs, lib.rs, set_migrated_tests.rs.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
Anchor does not generate ctx.bumps entries for accounts that specify
seeds::program pointing at the current program (equivalent to the
default, but triggers the exclusion). Removing the redundant constraint
restores the Bumps trait impl and fixes the build.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
The account that funds UserDelegateState init_if_needed creation is not
the spender debitor — it is a separate payer supplied by the caller.
Rename the field from `debitor` to `payer` throughout cpi_transfer to
align with the spender change that introduces a dedicated payer account,
decoupling rent from the global debitor and removing the shared writable
choke point exploited by the CostTracker griefing attack.

Audit finding: shared writable debitor enables CostTracker DoS
(Impact: Medium, Likelihood: Low).


Committed-By-Agent: claude

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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