2026-03-17 18:08:53 +01:00
|
|
|
pub use nssa_core::program::PdaSeed;
|
|
|
|
|
use nssa_core::{
|
|
|
|
|
account::{AccountId, AccountWithMetadata},
|
|
|
|
|
program::ProgramId,
|
|
|
|
|
};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub enum Instruction {
|
2026-05-13 17:24:11 -03:00
|
|
|
/// Create the Associated Token Account for (token program, owner, definition).
|
2026-03-17 18:08:53 +01:00
|
|
|
/// Idempotent: no-op if the account already exists.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts (3):
|
|
|
|
|
/// - Owner account
|
|
|
|
|
/// - Token definition account
|
|
|
|
|
/// - Associated token account (default/uninitialized, or already initialized)
|
|
|
|
|
///
|
2026-05-13 17:24:11 -03:00
|
|
|
/// `token_program_id` is explicit so callers can support multiple token programs without
|
|
|
|
|
/// letting account metadata choose downstream code.
|
|
|
|
|
Create { token_program_id: ProgramId },
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
/// Transfer tokens FROM owner's ATA to a recipient token holding account.
|
|
|
|
|
/// Uses ATA PDA seeds to authorize the chained Token::Transfer call.
|
2026-03-17 18:08:53 +01:00
|
|
|
///
|
|
|
|
|
/// Required accounts (3):
|
|
|
|
|
/// - Owner account (authorized)
|
|
|
|
|
/// - Sender ATA (owner's token holding)
|
fix(ata): lock down `ATA::Transfer` recipient contract
Enforce at the ATA layer that the recipient token holding is already
initialized, owned by the same token program as the sender ATA, decodes
to a valid `TokenHolding`, and points at the same token definition as
the sender. Align the core instruction doc and guest wrapper doc with
that contract, and cover the boundary with unit tests (default,
foreign-owned, malformed, mismatched-definition recipients, plus the
missing-owner-auth and happy paths) and end-to-end integration tests
(default and mismatched-definition recipients).
Without this, the downstream `token::Transfer` default-recipient
`Claim::Authorized` path was reachable through ATA, so integrators had
to reverse-engineer recipient semantics from token/runtime internals.
2026-05-11 12:44:46 -03:00
|
|
|
/// - Recipient token holding. Must be:
|
|
|
|
|
/// - already initialized (not a default account),
|
|
|
|
|
/// - owned by the same token program as the sender ATA,
|
|
|
|
|
/// - and point at the same token definition as the sender.
|
2026-03-17 18:08:53 +01:00
|
|
|
///
|
2026-05-13 17:24:11 -03:00
|
|
|
/// `token_program_id` is explicit so callers can support multiple token programs without
|
|
|
|
|
/// letting account metadata choose downstream code.
|
|
|
|
|
Transfer {
|
|
|
|
|
token_program_id: ProgramId,
|
|
|
|
|
amount: u128,
|
|
|
|
|
},
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
/// Burn tokens FROM owner's ATA.
|
|
|
|
|
/// Uses PDA seeds to authorize the ATA in the chained Token::Burn call.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts (3):
|
|
|
|
|
/// - Owner account (authorized)
|
|
|
|
|
/// - Owner's ATA (the holding to burn from)
|
|
|
|
|
/// - Token definition account
|
|
|
|
|
///
|
2026-05-13 17:24:11 -03:00
|
|
|
/// `token_program_id` is explicit so callers can support multiple token programs without
|
|
|
|
|
/// letting account metadata choose downstream code.
|
|
|
|
|
Burn {
|
|
|
|
|
token_program_id: ProgramId,
|
|
|
|
|
amount: u128,
|
|
|
|
|
},
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
pub fn compute_ata_seed(
|
|
|
|
|
token_program_id: ProgramId,
|
|
|
|
|
owner_id: AccountId,
|
|
|
|
|
definition_id: AccountId,
|
|
|
|
|
) -> PdaSeed {
|
2026-03-17 18:08:53 +01:00
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
2026-05-13 17:24:11 -03:00
|
|
|
let mut bytes = [0u8; 96];
|
|
|
|
|
for (index, word) in token_program_id.iter().enumerate() {
|
|
|
|
|
let offset = index * 4;
|
|
|
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
bytes[32..64].copy_from_slice(&owner_id.to_bytes());
|
|
|
|
|
bytes[64..96].copy_from_slice(&definition_id.to_bytes());
|
2026-03-17 18:08:53 +01:00
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_associated_token_account_id(ata_program_id: &ProgramId, seed: &PdaSeed) -> AccountId {
|
2026-05-11 15:29:41 +02:00
|
|
|
AccountId::for_public_pda(ata_program_id, seed)
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
/// Verify the ATA's address matches `(ata_program_id, token_program_id, owner, definition)` and
|
|
|
|
|
/// return the [`PdaSeed`] for use in chained calls.
|
2026-03-17 18:08:53 +01:00
|
|
|
pub fn verify_ata_and_get_seed(
|
|
|
|
|
ata_account: &AccountWithMetadata,
|
|
|
|
|
owner: &AccountWithMetadata,
|
2026-05-13 17:24:11 -03:00
|
|
|
token_program_id: ProgramId,
|
2026-03-17 18:08:53 +01:00
|
|
|
definition_id: AccountId,
|
|
|
|
|
ata_program_id: ProgramId,
|
|
|
|
|
) -> PdaSeed {
|
2026-05-13 17:24:11 -03:00
|
|
|
let seed = compute_ata_seed(token_program_id, owner.account_id, definition_id);
|
2026-03-17 18:08:53 +01:00
|
|
|
let expected_id = get_associated_token_account_id(&ata_program_id, &seed);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ata_account.account_id, expected_id,
|
|
|
|
|
"ATA account ID does not match expected derivation"
|
|
|
|
|
);
|
|
|
|
|
seed
|
|
|
|
|
}
|