Skip to content

fix(token): reject foreign-owned definitions on initialize#82

Open
3esmit wants to merge 4 commits intomainfrom
fix/initialize-account-definition-owner
Open

fix(token): reject foreign-owned definitions on initialize#82
3esmit wants to merge 4 commits intomainfrom
fix/initialize-account-definition-owner

Conversation

@3esmit
Copy link
Copy Markdown
Collaborator

@3esmit 3esmit commented May 4, 2026

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_id from the LEZ program input and passes it into the token initialize path. The public InitializeAccount instruction shape is unchanged, and the generated token IDL still matches the committed artifact.

Changes

  • Added a token-program ownership check before initialize_account deserializes the token definition and derives the zeroized holding.
  • Threaded self_program_id from the token guest into the token initialize implementation without adding a caller-supplied instruction argument.
  • Kept the token guest IDL annotations available for idl-gen while using explicit guest dispatch for the runtime path that needs self_program_id.
  • Added unit coverage for a Borsh-valid TokenDefinition owned by a foreign program.
  • Added integration coverage for a public InitializeAccount transaction through the token guest path, including a check that the destination account remains uninitialized after rejection.

Validation

The following checks passed:

cargo +nightly fmt --all -- --check
taplo fmt --check .
RISC0_SKIP_BUILD=1 cargo +1.94.0 clippy --workspace --all-targets -- -D warnings
RISC0_DEV_MODE=1 cargo +1.94.0 test --workspace --exclude integration_tests
RISC0_DEV_MODE=1 cargo +1.94.0 test -p integration_tests
cargo +1.94.0 run -p idl-gen -- token/methods/guest/src/bin/token.rs > /tmp/lez-token-idl.json
diff -u artifacts/token-idl.json /tmp/lez-token-idl.json

Copilot AI review requested due to automatic review settings May 4, 2026 13:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id into 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.

Comment thread token/src/initialize.rs
Comment thread token/methods/guest/src/bin/token.rs
Comment thread integration_tests/tests/token.rs

// Kept for `idl-gen`, which parses `#[lez_program]` source annotations directly.
#[cfg(test)]
#[allow(dead_code)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

@3esmit 3esmit May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

3esmit added 3 commits May 4, 2026 13:04
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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Comment on lines +41 to +108
#[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)
}
}
Comment thread token/src/tests.rs
Comment on lines +28 to +30
const TOKEN_PROGRAM_ID: ProgramId = [5u32; 8];
const FOREIGN_TOKEN_PROGRAM_ID: ProgramId = [6u32; 8];

Comment on lines +62 to +77
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,
)
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.

Reject foreign-owned token definitions in InitializeAccount

3 participants