feat: protect from public pda griefing attacks

This commit is contained in:
Daniil Polyakov
2026-03-28 01:23:57 +03:00
parent 085ca69e42
commit 6780f1c9a4
51 changed files with 639 additions and 301 deletions
+28 -12
View File
@@ -2,11 +2,11 @@ use std::num::NonZeroU128;
use amm_core::{
PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
compute_pool_pda, compute_vault_pda,
compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed,
};
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{AccountPostState, ChainedCall, ProgramId},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
@@ -108,36 +108,52 @@ pub fn new_definition(
};
pool_post.data = Data::from(&pool_post_definition);
let pool_post = AccountPostState::new_claimed_if_default(pool_post);
let pool_pda_seed = compute_pool_pda_seed(definition_token_a_id, definition_token_b_id);
let pool_post = AccountPostState::new_claimed_if_default(pool_post, Claim::Pda(pool_pda_seed));
let token_program_id = user_holding_a.account.program_owner;
// Chain call for Token A (user_holding_a -> Vault_A)
let vault_a_seed = compute_vault_pda_seed(pool.account_id, definition_token_a_id);
let vault_a_authorized = AccountWithMetadata {
is_authorized: true,
..vault_a.clone()
};
let call_token_a = ChainedCall::new(
token_program_id,
vec![user_holding_a.clone(), vault_a.clone()],
vec![user_holding_a.clone(), vault_a_authorized],
&token_core::Instruction::Transfer {
amount_to_transfer: token_a_amount.into(),
},
);
)
.with_pda_seeds(vec![vault_a_seed]);
// Chain call for Token B (user_holding_b -> Vault_B)
let vault_b_seed = compute_vault_pda_seed(pool.account_id, definition_token_b_id);
let vault_b_authorized = AccountWithMetadata {
is_authorized: true,
..vault_b.clone()
};
let call_token_b = ChainedCall::new(
token_program_id,
vec![user_holding_b.clone(), vault_b.clone()],
vec![user_holding_b.clone(), vault_b_authorized],
&token_core::Instruction::Transfer {
amount_to_transfer: token_b_amount.into(),
},
);
let mut pool_lp_auth = pool_definition_lp.clone();
pool_lp_auth.is_authorized = true;
)
.with_pda_seeds(vec![vault_b_seed]);
let pool_lp_pda_seed = compute_liquidity_token_pda_seed(pool.account_id);
let pool_lp_authorized = AccountWithMetadata {
is_authorized: true,
..pool_definition_lp.clone()
};
let call_token_lp = ChainedCall::new(
token_program_id,
vec![pool_lp_auth, user_holding_lp.clone()],
vec![pool_lp_authorized, user_holding_lp.clone()],
&instruction,
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
.with_pda_seeds(vec![pool_lp_pda_seed]);
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
+7 -5
View File
@@ -1,4 +1,4 @@
use std::num::NonZero;
use std::{num::NonZero, vec};
use amm_core::{
PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
@@ -1756,7 +1756,7 @@ impl AccountsForExeTests {
definition_id: IdForExeTests::token_lp_definition_id(),
balance: BalanceForExeTests::lp_supply_init(),
}),
nonce: 0_u128.into(),
nonce: 1_u128.into(),
}
}
@@ -1801,7 +1801,7 @@ impl AccountsForExeTests {
definition_id: IdForExeTests::token_lp_definition_id(),
balance: 0,
}),
nonce: 0_u128.into(),
nonce: 1.into(),
}
}
}
@@ -2799,7 +2799,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
IdForExeTests::user_token_b_id(),
IdForExeTests::user_token_lp_id(),
],
vec![0_u128.into(), 0_u128.into()],
vec![0_u128.into(), 0_u128.into(), 0_u128.into()],
instruction,
)
.unwrap();
@@ -2809,6 +2809,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
&[
&PrivateKeysForTests::user_token_a_key(),
&PrivateKeysForTests::user_token_b_key(),
&PrivateKeysForTests::user_token_lp_key(),
],
);
@@ -2955,7 +2956,7 @@ fn simple_amm_new_definition_uninitialized_pool() {
IdForExeTests::user_token_b_id(),
IdForExeTests::user_token_lp_id(),
],
vec![0_u128.into(), 0_u128.into()],
vec![0_u128.into(), 0_u128.into(), 0_u128.into()],
instruction,
)
.unwrap();
@@ -2965,6 +2966,7 @@ fn simple_amm_new_definition_uninitialized_pool() {
&[
&PrivateKeysForTests::user_token_a_key(),
&PrivateKeysForTests::user_token_b_key(),
&PrivateKeysForTests::user_token_lp_key(),
],
);
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, ChainedCall, ProgramId},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
pub fn create_associated_token_account(
@@ -11,7 +11,7 @@ pub fn create_associated_token_account(
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
// No authorization check needed: create is idempotent, so anyone can call it safely.
let token_program_id = token_definition.account.program_owner;
ata_core::verify_ata_and_get_seed(
let ata_seed = ata_core::verify_ata_and_get_seed(
&ata_account,
&owner,
token_definition.account_id,
@@ -22,7 +22,7 @@ pub fn create_associated_token_account(
if ata_account.account != Account::default() {
return (
vec![
AccountPostState::new_claimed_if_default(owner.account.clone()),
AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized),
AccountPostState::new(token_definition.account.clone()),
AccountPostState::new(ata_account.account.clone()),
],
@@ -31,14 +31,20 @@ pub fn create_associated_token_account(
}
let post_states = vec![
AccountPostState::new_claimed_if_default(owner.account.clone()),
AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized),
AccountPostState::new(token_definition.account.clone()),
AccountPostState::new(ata_account.account.clone()),
];
let ata_account_auth = AccountWithMetadata {
is_authorized: true,
..ata_account.clone()
};
let chained_call = ChainedCall::new(
token_program_id,
vec![token_definition.clone(), ata_account.clone()],
vec![token_definition.clone(), ata_account_auth],
&token_core::Instruction::InitializeAccount,
);
)
.with_pda_seeds(vec![ata_seed]);
(post_states, vec![chained_call])
}
+14 -14
View File
@@ -10,23 +10,23 @@ pub enum Instruction {
/// Transfer tokens from sender to recipient.
///
/// Required accounts:
/// - Sender's Token Holding account (authorized),
/// - Recipient's Token Holding account.
/// - Sender's Token Holding account (initialized, authorized),
/// - Recipient's Token Holding account (initialized or authorized and uninitialized).
Transfer { amount_to_transfer: u128 },
/// Create a new fungible token definition without metadata.
///
/// Required accounts:
/// - Token Definition account (uninitialized),
/// - Token Holding account (uninitialized).
/// - Token Definition account (uninitialized, authorized),
/// - Token Holding account (uninitialized, authorized).
NewFungibleDefinition { name: String, total_supply: u128 },
/// Create a new fungible or non-fungible token definition with metadata.
///
/// Required accounts:
/// - Token Definition account (uninitialized),
/// - Token Holding account (uninitialized),
/// - Token Metadata account (uninitialized).
/// - Token Definition account (uninitialized, authorized),
/// - Token Holding account (uninitialized, authorized),
/// - Token Metadata account (uninitialized, authorized).
NewDefinitionWithMetadata {
new_definition: NewTokenDefinition,
/// Boxed to avoid large enum variant size.
@@ -36,29 +36,29 @@ pub enum Instruction {
/// Initialize a token holding account for a given token definition.
///
/// Required accounts:
/// - Token Definition account (initialized),
/// - Token Holding account (uninitialized),
/// - Token Definition account (initialized, any authorization),
/// - Token Holding account (uninitialized, authorized),
InitializeAccount,
/// Burn tokens from the holder's account.
///
/// Required accounts:
/// - Token Definition account (initialized),
/// - Token Holding account (authorized).
/// - Token Definition account (initialized, any authorization),
/// - Token Holding account (initialized, authorized).
Burn { amount_to_burn: u128 },
/// Mint new tokens to the holder's account.
///
/// Required accounts:
/// - Token Definition account (authorized),
/// - Token Holding account (uninitialized or initialized).
/// - Token Definition account (initialized, authorized),
/// - Token Holding account (uninitialized or authorized and initialized).
Mint { amount_to_mint: u128 },
/// Print a new NFT from the master copy.
///
/// Required accounts:
/// - NFT Master Token Holding account (authorized),
/// - NFT Printed Copy Token Holding account (uninitialized).
/// - NFT Printed Copy Token Holding account (uninitialized, authorized).
PrintNft,
}
+2 -2
View File
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{TokenDefinition, TokenHolding};
@@ -30,6 +30,6 @@ pub fn initialize_account(
vec![
AccountPostState::new(definition_post),
AccountPostState::new_claimed(account_to_initialize),
AccountPostState::new_claimed(account_to_initialize, Claim::Authorized),
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{TokenDefinition, TokenHolding};
@@ -67,6 +67,6 @@ pub fn mint(
vec![
AccountPostState::new(definition_post),
AccountPostState::new_claimed_if_default(holding_post),
AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized),
]
}
+6 -6
View File
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{
NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata,
@@ -42,8 +42,8 @@ pub fn new_fungible_definition(
holding_target_account_post.data = Data::from(&token_holding);
vec![
AccountPostState::new_claimed(definition_target_account_post),
AccountPostState::new_claimed(holding_target_account_post),
AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized),
]
}
@@ -119,8 +119,8 @@ pub fn new_definition_with_metadata(
metadata_target_account_post.data = Data::from(&token_metadata);
vec![
AccountPostState::new_claimed(definition_target_account_post),
AccountPostState::new_claimed(holding_target_account_post),
AccountPostState::new_claimed(metadata_target_account_post),
AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized),
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::TokenHolding;
@@ -50,6 +50,6 @@ pub fn print_nft(
vec![
AccountPostState::new(master_account_post),
AccountPostState::new_claimed(printed_account_post),
AccountPostState::new_claimed(printed_account_post, Claim::Authorized),
]
}
+5 -2
View File
@@ -5,7 +5,10 @@
reason = "We don't care about it in tests"
)]
use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data},
program::Claim,
};
use token_core::{
MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding,
};
@@ -851,7 +854,7 @@ fn mint_uninit_holding_success() {
*holding_post.account(),
AccountForTests::init_mint().account
);
assert!(holding_post.requires_claim());
assert_eq!(holding_post.required_claim(), Some(Claim::Authorized));
}
#[test]
+2 -2
View File
@@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::TokenHolding;
@@ -106,6 +106,6 @@ pub fn transfer(
vec![
AccountPostState::new(sender_post),
AccountPostState::new_claimed_if_default(recipient_post),
AccountPostState::new_claimed_if_default(recipient_post, Claim::Authorized),
]
}