Organization
- @coinbase
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Informational
6 findings
5 fixed
1 acknowledged
Informational6 findings
Canonical IDL initialization can be front run
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The program declares Anchor's
no-idlfeature but does not enable it in the default production feature set. Anchor0.31.1therefore adds a legacy instruction namespace to the compiled program for creating, writing, replacing, transferring, and closing a canonical IDL account.The generated
Createhandler accepts any fundedfromsigner. It verifies that the supplied executable program is the current program, creates the canonical IDL address, and stores the first signer's key as its authority. It does not require the program data upgrade authority, the Mint Controller global admin, or the deployer.This path was reproduced against the exact reviewed ELF. An unrelated signer created the canonical IDL account, wrote synthetic compressed JSON, and made the locked Anchor client's
Program.fetchIdlreturn it. A later legitimate initializer failed because the account already existed, and its write failed the generatedhas_one = authorityconstraint.Program.atthen accepted the forged IDL's embedded address and constructed a client for an attacker selected program ID.The attacker cannot modify Mint Controller state, replace executable code, or mint tokens through this path. However, wallets, explorers, decoders, account resolvers, or signing tools that trust the canonical IDL can display or construct an attacker selected schema. No production consumer in this repository was shown to rely on this path, so the issue is informational.
Recommendation
Enable
no-idlin the crate's default feature set so the production artifact cannot include the legacy namespace because a deployment command omitted a build flag. Continue usingidl-buildto generate the client side IDL, and publish it as a hash attested release artifact throughinventory_metadata.source_code_data.Add a release check that builds the exact production feature set and verifies that the legacy IDL
Createpayload is rejected withIdlInstructionStub. This preserves Anchor client and TypeScript IDL generation without leaving the canonical IDL account available to the first caller.The program is not deployed yet, so this can be corrected before any canonical IDL account exists. If that assumption changes, inspect the account, authority, and content before enabling
no-idl, because removing the handlers does not remove or recover an existing account.Negative tests can catch their own failure assertions
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
Most negative tests put
assert.fail("expected <error>")inside the sametryblock whosecatchchecks the thrown string for<error>. If the RPC unexpectedly succeeds, Chai throws a local assertion error containing the expected error name, and thecatchblock can accept that local assertion as though it came from the Solana program.The issue was reproduced with the checked in Chai dependency:
AssertionError: expected InvalidMintAuthoritycatch assertion PASSEDThe affected fail sites include lines
243,279,407,423,465,482,496,510,523,535,605,642,675,727,742,783,847,878,941,972,1048, and1103. The later exact balance assertions in the cases containing lines642and1048still detect an unexpected successful mint, but their local error oracle accepts the wrong exception and cannot prove the expected program error. The other twenty cases lack an equivalent postcondition.These tests appear to cover authorization, validation, allowlist, pause, and rate limit failures, but they can report success without observing the expected program rejection. A future regression could therefore pass the checked in test suite.
Recommendation
Replace all 25
assert.failsites with a rejection helper that catches only errors thrown by the awaited RPC promise, throws outside thecatchpath if the promise resolves, and asserts the exact Anchor error code rather than broad message substrings. Rerun every authorization, validation, allowlist, pause, and rate limit negative test after making the change.Solana rolls back all writes when a transaction fails, so a complete account snapshot is not necessary in every administrative negative test. Add exact supply, destination balance, and rate limit capacity postconditions to the mint path negatives, where an unexpected success would move value. Keep broader rollback snapshots for focused CPI failure and multiple instruction atomicity tests.
README misstates allowlist authority and lifecycle
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The README says the per mint
adminmanages recipient allowlists and listsadminas the caller foradd_allowed_mint_recipientandremove_allowed_mint_recipient. The code enforcesallowlist_authorityfor those instructions through theAddAllowedMintRecipientandRemoveAllowedMintRecipientaccount contexts.The README also says
add_allowed_mint_recipientlazily creates the allowlist, while the implementation creates the allowlist inconfigure_minterviainit_if_needed. Finally, the preflight sequence documentsinitialize(admin, rate_limit_authority)even though the instruction also requiresallowlist_authority.Operators can assign or rotate the wrong keys and then fail to execute allowlist operations during deployment or incident response. This is a documentation and operator guidance mismatch rather than an authorization bypass in the program.
Recommendation
Correct the README sections that describe the allowlist role and lifecycle:
- Add
allowlist_authorityto the roles table and to the documentedMintRolesPDA fields. - Document
update_allowlist_authorityin the instruction table. - State that
allowlist_authority, rather than the per mintadmin, controls add and remove allowlist operations. - State that
configure_mintercreates both the rate limit and allowlist PDAs, and thatrevoke_minteralways closes both. - Update the initialization signature and deployment preflight to include
admin,rate_limit_authority, andallowlist_authority.
Mint authority handoff occurs before controller setup
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The README instructs operators to transfer SPL mint authority to the program's
mint_authorityPDA before callinginitialize. The program requires that order becauseinitializerejects a mint unless its current mint authority is already the PDA.After the authority transfer, only the program can sign for that PDA. The program exposes
mint_tothroughmint_tokens, but it does not expose a governed SPL Tokenset_authorityCPI that can return or rotate mint authority. If initialization cannot complete because of a wrong program ID, unsupported token program, unavailable global admin, incorrect build artifact, or another deployment issue, there is no source level recovery path for the mint authority. Recovery then depends on the upgrade authority and a new binary if the program remains upgradeable.An onboarding failure after the handoff can leave issuance unavailable for the affected mint because the original authority can no longer recover the mint. This does not let an unauthorized account mint tokens.
Recommendation
Adopt the proposed prepare then handoff deployment order. Remove the mint authority requirement from
initialize, then initialize the mint roles, configure its minters, populate the allowlists, and prove that the critical role keys can sign while the original authority still controls the mint.Before the final handoff, verify the exact deployed program address and binary hash, derive the
mint_authorityPDA from that deployed address, and confirm that the mint is owned by the classic SPL Token program. Makespl-token authorizethe final deployment step. Retain the existing mint authority constraint inMintTokens, so controller minting remains disabled until the handoff succeeds.Add integration tests proving that
mint_tokensrejects before the handoff, failed setup leaves the original mint authority recoverable, and the final authority transfer enables minting through the controller. A permanent decommission instruction is not required if the deployment pipeline enforces these checks and this ordering.Single-step global admin rotation has no recovery path
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Jiri123
Description
update_global_admin overwrites admin in one call, checking only that the new key is non-zero. A wrong but valid key takes control immediately, and since GlobalConfig cannot be closed or re-initialized, recovery needs a program upgrade.
Recommendation
update_global_admin sets a pending_admin, and a new accept_global_admin signed by that key completes the transfer. Same as Ownable2Step on the EVM side.
Rent funding expectation for minter PDAs is undocumented
State
Severity
- Severity: Informational
Submitted by
Jiri123
Description
ConfigureMinter sets payer = rate_limit_authority for both PDAs, while RevokeMinter uses close = admin. When the roles are separate keys, about 0.0247 SOL per minter moves from one to the other each grant/revoke cycle.
Recommendation
Document in the README preflight that rate_limit_authority pays the rent and admin reclaims it, and that rent goes to whoever holds the admin role at close time rather than the original payer.