fix(token): reject foreign-owned definitions on initialize#82
fix(token): reject foreign-owned definitions on initialize#82
Conversation
There was a problem hiding this comment.
Pull request overview
Hardens token account initialization against foreign-owned definition accounts by threading the executing program ID through the token guest and enforcing the ownership check in the token program. This fits the token program’s trust-boundary checks so holdings created during initialization are tied to the canonical token program.
Changes:
- Added a token-program ownership check in
initialize_account. - Switched the token guest runtime to explicit dispatch so it can pass
self_program_idinto initialization without changing the public instruction shape. - Added unit and integration tests for rejecting foreign-owned definitions during
InitializeAccount.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| token/src/tests.rs | Updates initialize unit tests for the new parameter and adds a foreign-owner rejection case. |
| token/src/initialize.rs | Adds the definition owner check before creating a zeroized holding. |
| token/methods/guest/src/bin/token.rs | Reworks the guest entrypoint to manually read program input and pass self_program_id into initialize dispatch. |
| integration_tests/tests/token.rs | Adds an end-to-end rejection test for foreign-owned definitions through a public transaction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Kept for `idl-gen`, which parses `#[lez_program]` source annotations directly. | ||
| #[cfg(test)] | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
This looks like a workaround. We should rather add support for self_program_id to be passed to the program by SPEL via lez_program
There was a problem hiding this comment.
Agreed. The explicit guest dispatch was only there to avoid adding a caller-supplied program id to the public token instruction, but the cleaner fix is for SPEL to expose the trusted self_program_id to instruction handlers directly.
There was a problem hiding this comment.
I opened logos-co/spel#172 to track support for SPEL's #[lez_program] generated dispatch to expose trusted execution context, including self_program_id, to #[instruction] handlers without adding caller-controlled instruction arguments.
Keep the manual dispatcher for self_program_id checks while allowing the SPEL IDL wrapper to compile during tests without defining a second entrypoint. See logos-co/spel#172
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) -> Vec<AccountPostState> { | ||
| match instruction { | ||
| TokenInstruction::Transfer { amount_to_transfer } => { | ||
| let [sender, recipient] = expect_accounts(pre_states); |
| ) | ||
| } | ||
| TokenInstruction::PrintNft => { | ||
| let [master_account, printed_account] = expect_accounts(pre_states); |
| #[cfg_attr(test, allow(dead_code))] | ||
| fn dispatch( | ||
| instruction: TokenInstruction, | ||
| pre_states: Vec<AccountWithMetadata>, | ||
| self_program_id: ProgramId, | ||
| ) -> Vec<AccountPostState> { | ||
| match instruction { | ||
| TokenInstruction::Transfer { amount_to_transfer } => { | ||
| let [sender, recipient] = expect_accounts(pre_states); | ||
| token_program::transfer::transfer(sender, recipient, amount_to_transfer) | ||
| } | ||
| TokenInstruction::NewFungibleDefinition { name, total_supply } => { | ||
| let [definition_target_account, holding_target_account] = | ||
| expect_accounts(pre_states); | ||
| token_program::new_definition::new_fungible_definition( | ||
| definition_target_account, | ||
| holding_target_account, | ||
| name, | ||
| total_supply, | ||
| ) | ||
| } | ||
| TokenInstruction::NewDefinitionWithMetadata { | ||
| new_definition, | ||
| metadata, | ||
| } => { | ||
| let [ | ||
| definition_target_account, | ||
| holding_target_account, | ||
| metadata_target_account, | ||
| ] = expect_accounts(pre_states); | ||
| token_program::new_definition::new_definition_with_metadata( | ||
| definition_target_account, | ||
| holding_target_account, | ||
| metadata_target_account, | ||
| new_definition, | ||
| *metadata, | ||
| ) | ||
| } | ||
| TokenInstruction::InitializeAccount => { | ||
| let [definition_account, account_to_initialize] = expect_accounts(pre_states); | ||
| token_program::initialize::initialize_account( | ||
| definition_account, | ||
| account_to_initialize, | ||
| self_program_id, | ||
| ) | ||
| } | ||
| TokenInstruction::Burn { amount_to_burn } => { | ||
| let [definition_account, user_holding_account] = expect_accounts(pre_states); | ||
| token_program::burn::burn( | ||
| definition_account, | ||
| user_holding_account, | ||
| amount_to_burn, | ||
| ) | ||
| } | ||
| TokenInstruction::Mint { amount_to_mint } => { | ||
| let [definition_account, user_holding_account] = expect_accounts(pre_states); | ||
| token_program::mint::mint( | ||
| definition_account, | ||
| user_holding_account, | ||
| amount_to_mint, | ||
| self_program_id, | ||
| ) | ||
| } | ||
| TokenInstruction::PrintNft => { | ||
| let [master_account, printed_account] = expect_accounts(pre_states); | ||
| token_program::print_nft::print_nft(master_account, printed_account) | ||
| } | ||
| } |
| const TOKEN_PROGRAM_ID: ProgramId = [5u32; 8]; | ||
| const FOREIGN_TOKEN_PROGRAM_ID: ProgramId = [6u32; 8]; | ||
|
|
| TokenInstruction::NewDefinitionWithMetadata { | ||
| new_definition, | ||
| metadata, | ||
| } => { | ||
| let [ | ||
| definition_target_account, | ||
| holding_target_account, | ||
| metadata_target_account, | ||
| ] = expect_accounts(pre_states); | ||
| token_program::new_definition::new_definition_with_metadata( | ||
| definition_target_account, | ||
| holding_target_account, | ||
| metadata_target_account, | ||
| new_definition, | ||
| *metadata, | ||
| ) |
Closes #80
Summary
This PR makes token account initialization reject token definition accounts that are not owned by the token program currently executing
InitializeAccount.The token guest now reads the trusted
self_program_idfrom the LEZ program input and passes it into the token initialize path. The publicInitializeAccountinstruction shape is unchanged, and the generated token IDL still matches the committed artifact.Changes
initialize_accountdeserializes the token definition and derives the zeroized holding.self_program_idfrom the token guest into the token initialize implementation without adding a caller-supplied instruction argument.idl-genwhile using explicit guest dispatch for the runtime path that needsself_program_id.TokenDefinitionowned by a foreign program.InitializeAccounttransaction through the token guest path, including a check that the destination account remains uninitialized after rejection.Validation
The following checks passed: