Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ ci-integration-test-prebuilt:
@cd integration && cargo test --test mint_idempotency_integration -- --nocapture
@cd integration && cargo test --test gap_detection_integration -- --nocapture
@cd integration && cargo test --test truncate_integration -- --nocapture
@cd integration && cargo test --test pausable_mint_integration -- --nocapture
@cd integration && cargo test --test permanent_delegate_mint_integration -- --nocapture
@cd integration && cargo test --test resync_integration -- --nocapture
@cd integration && cargo test --test reconciliation_e2e_test -- --nocapture
@cd integration && cargo test --test mock_rpc_retry -- --nocapture
Expand Down Expand Up @@ -157,6 +159,8 @@ ci-integration-test-indexer:
@cd integration && cargo test --test mint_idempotency_integration -- --nocapture
@cd integration && cargo test --test gap_detection_integration -- --nocapture
@cd integration && cargo test --test truncate_integration -- --nocapture
@cd integration && cargo test --test pausable_mint_integration -- --nocapture
@cd integration && cargo test --test permanent_delegate_mint_integration -- --nocapture
@cd integration && cargo test --test resync_integration -- --nocapture
@cd integration && cargo test --test reconciliation_e2e_test -- --nocapture
@cd integration && cargo test --test mock_rpc_retry -- --nocapture
Expand Down
7 changes: 4 additions & 3 deletions contra-escrow-program/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
- `test_allow_mint_invalid_admin` — wrong admin rejected
- `test_allow_mint_invalid_instance_account_owner` — wrong owner rejected
- `test_allow_mint_token_2022_basic_success` — Token2022 mint allowed
- `test_allow_mint_token_2022_permanent_delegate_blocked` — PermanentDelegateNotAllowed
- `test_allow_mint_token_2022_pausable_blocked` — PausableMintNotAllowed
- `test_allow_mint_token_2022_permanent_delegate_accepted` — permanent-delegate Token-2022 mint allowed; drain detection is enforced by the operator at withdrawal time
- `test_allow_mint_token_2022_pausable_accepted` — pausable Token-2022 mint allowed; pause state is enforced by the operator at withdrawal time
- `test_allow_mint_token_2022_transfer_hook_blocked` — TransferHookNotAllowed; the program's `TransferChecked` CPI does not resolve extra-account metas, so hook mints are rejected at validation

### BlockMint (9 integration tests)

Expand Down Expand Up @@ -90,7 +91,7 @@
- `test_deposit_invalid_instruction_data_too_short` — malformed data
- `test_deposit_not_enough_accounts` — missing accounts
- `test_deposit_token_2022_basic_success` — Token2022 deposit
- `test_deposit_token_2022_permanent_delegate_rejected` — Token2022 extension blocked
- `test_deposit_token_2022_transfer_hook_rejected` — TransferHookNotAllowed on deposit path (live swap of mint data post-AllowMint proves the check runs at deposit, not only at AllowMint)
- `test_deposit_invalid_associated_token_program` — wrong ATA program rejected
- `test_multiple_depositors_same_instance` — three users deposit to same instance
- `test_deposit_wrong_user_ata` — passing another user's ATA as the user_ata is rejected with InvalidInstructionData
Expand Down
20 changes: 7 additions & 13 deletions contra-escrow-program/idl/contra_escrow_program.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,47 +514,41 @@
{
"code": 6,
"kind": "errorNode",
"message": "Permanent delegate extension not allowed",
"name": "permanentDelegateNotAllowed"
"message": "Transfer hook extension not allowed",
"name": "transferHookNotAllowed"
},
{
"code": 7,
"kind": "errorNode",
"message": "Pausable mint extension not allowed",
"name": "pausableMintNotAllowed"
},
{
"code": 8,
"kind": "errorNode",
"message": "Invalid operator PDA provided",
"name": "invalidOperatorPda"
},
{
"code": 9,
"code": 8,
"kind": "errorNode",
"message": "Invalid token account provided",
"name": "invalidTokenAccount"
},
{
"code": 10,
"code": 9,
"kind": "errorNode",
"message": "Invalid escrow balance",
"name": "invalidEscrowBalance"
},
{
"code": 11,
"code": 10,
"kind": "errorNode",
"message": "Invalid allowed mint",
"name": "invalidAllowedMint"
},
{
"code": 12,
"code": 11,
"kind": "errorNode",
"message": "Invalid SMT proof provided",
"name": "invalidSmtProof"
},
{
"code": 13,
"code": 12,
"kind": "errorNode",
"message": "Invalid transaction nonce for current tree index",
"name": "invalidTransactionNonceForCurrentTreeIndex"
Expand Down
37 changes: 16 additions & 21 deletions contra-escrow-program/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,31 @@ pub enum ContraEscrowProgramError {
#[error("Invalid admin provided")]
InvalidAdmin,

/// (6) Permanent delegate extension not allowed
#[error("Permanent delegate extension not allowed")]
PermanentDelegateNotAllowed,
/// (6) Transfer hook extension not allowed
#[error("Transfer hook extension not allowed")]
TransferHookNotAllowed,

/// (7) Pausable mint extension not allowed
#[error("Pausable mint extension not allowed")]
PausableMintNotAllowed,

/// (8) Invalid operator PDA provided
/// (7) Invalid operator PDA provided
#[error("Invalid operator PDA provided")]
InvalidOperatorPda,

/// (9) Invalid token account provided
/// (8) Invalid token account provided
#[error("Invalid token account provided")]
InvalidTokenAccount,

/// (10) Invalid escrow balance
/// (9) Invalid escrow balance
#[error("Invalid escrow balance")]
InvalidEscrowBalance,

/// (11) Invalid allowed mint
/// (10) Invalid allowed mint
#[error("Invalid allowed mint")]
InvalidAllowedMint,

/// (12) Invalid SMT proof provided
/// (11) Invalid SMT proof provided
#[error("Invalid SMT proof provided")]
InvalidSmtProof,

/// (13) Invalid transaction nonce for current tree index
/// (12) Invalid transaction nonce for current tree index
#[error("Invalid transaction nonce for current tree index")]
InvalidTransactionNonceForCurrentTreeIndex,
}
Expand Down Expand Up @@ -87,14 +83,13 @@ mod tests {
(InvalidInstanceId, 3),
(InvalidInstance, 4),
(InvalidAdmin, 5),
(PermanentDelegateNotAllowed, 6),
(PausableMintNotAllowed, 7),
(InvalidOperatorPda, 8),
(InvalidTokenAccount, 9),
(InvalidEscrowBalance, 10),
(InvalidAllowedMint, 11),
(InvalidSmtProof, 12),
(InvalidTransactionNonceForCurrentTreeIndex, 13),
(TransferHookNotAllowed, 6),
(InvalidOperatorPda, 7),
(InvalidTokenAccount, 8),
(InvalidEscrowBalance, 9),
(InvalidAllowedMint, 10),
(InvalidSmtProof, 11),
(InvalidTransactionNonceForCurrentTreeIndex, 12),
];

for (error, expected_code) in cases {
Expand Down
23 changes: 15 additions & 8 deletions contra-escrow-program/program/src/processor/shared/token_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ use pinocchio_token_2022::{
state::Mint as Token2022Mint, state::TokenAccount as Token2022Account,
ID as TOKEN_2022_PROGRAM_ID,
};
use spl_token_2022::extension::StateWithExtensions;
use spl_token_2022::extension::{
pausable::PausableConfig, permanent_delegate::PermanentDelegate, BaseStateWithExtensions,
transfer_hook::TransferHook, BaseStateWithExtensions, StateWithExtensions,
};
use spl_token_2022::state::Mint as Token2022MintState;

Expand Down Expand Up @@ -111,19 +110,27 @@ pub fn get_mint_decimals(mint_info: &AccountView) -> Result<u8, ProgramError> {
Err(ContraEscrowProgramError::InvalidMint.into())
}

/// Blocks mints with PermanentDelegate or Pausable extensions.
/// Validates the mint's data parses as a Token-2022 mint and rejects
/// mints carrying the `TransferHook` extension.
///
/// Pausable and permanent-delegate mints are accepted — the operator
/// enforces pause and drain states off-chain via a withdrawal pre-flight.
/// `TransferHook` is a different class of problem: honoring the hook would
/// require the program to resolve `ExtraAccountMetaList` PDAs and forward
/// them through the `TransferChecked` CPI, which the current pinocchio
/// builder does not do. Accepting such a mint would cause every deposit /
/// release to fail on-chain (Token-2022 invokes the hook program without
/// the required extra accounts), so we reject it at the validation seam
/// rather than let downstream transfers burn fees.
#[inline(always)]
pub fn validate_token2022_extensions(mint_info: &AccountView) -> ProgramResult {
let data = mint_info.try_borrow()?;

let mint = StateWithExtensions::<Token2022MintState>::unpack(&data)
.map_err(|_| ContraEscrowProgramError::InvalidMint)?;

if mint.get_extension::<PermanentDelegate>().is_ok() {
return Err(ContraEscrowProgramError::PermanentDelegateNotAllowed.into());
}
if mint.get_extension::<PausableConfig>().is_ok() {
return Err(ContraEscrowProgramError::PausableMintNotAllowed.into());
if mint.get_extension::<TransferHook>().is_ok() {
return Err(ContraEscrowProgramError::TransferHookNotAllowed.into());
}

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use crate::{
state_utils::{assert_get_or_allow_mint, assert_get_or_create_instance},
utils::{
assert_program_error, set_mint, set_mint_2022_basic, set_mint_2022_with_pausable,
set_mint_2022_with_permanent_delegate, TestContext, CONTRA_ESCROW_PROGRAM_ID,
INVALID_ACCOUNT_DATA_ERROR, INVALID_ADMIN_ERROR, INVALID_ALLOWED_MINT_ERROR,
MISSING_REQUIRED_SIGNATURE_ERROR, PAUSABLE_MINT_NOT_ALLOWED_ERROR,
PERMANENT_DELEGATE_NOT_ALLOWED_ERROR, TOKEN_2022_PROGRAM_ID,
set_mint_2022_with_permanent_delegate, set_mint_2022_with_transfer_hook, TestContext,
CONTRA_ESCROW_PROGRAM_ID, INVALID_ACCOUNT_DATA_ERROR, INVALID_ADMIN_ERROR,
INVALID_ALLOWED_MINT_ERROR, MISSING_REQUIRED_SIGNATURE_ERROR, TOKEN_2022_PROGRAM_ID,
TRANSFER_HOOK_NOT_ALLOWED_ERROR,
},
};
use contra_escrow_program_client::instructions::AllowMintBuilder;
Expand Down Expand Up @@ -321,7 +321,7 @@ fn test_allow_mint_token_2022_basic_success() {
}

#[test]
fn test_allow_mint_token_2022_permanent_delegate_blocked() {
fn test_allow_mint_token_2022_permanent_delegate_accepted() {
let mut context = TestContext::new();
let admin = Keypair::new();
let mint = Keypair::new();
Expand All @@ -334,48 +334,55 @@ fn test_allow_mint_token_2022_permanent_delegate_blocked() {
assert_get_or_create_instance(&mut context, &admin, &instance_seed, false, false)
.expect("CreateInstance should succeed");

context
.airdrop_if_required(&admin.pubkey(), 1_000_000_000)
.unwrap();

let (allowed_mint_pda, bump) = find_allowed_mint_pda(&instance_pda, &mint.pubkey());
let (event_authority_pda, _) = find_event_authority_pda();
let instance_ata = spl_associated_token_account::get_associated_token_address_with_program_id(
assert_get_or_allow_mint(
&mut context,
&admin,
&instance_pda,
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
false,
true,
)
.expect("AllowMint should succeed for a Token-2022 mint with a permanent delegate");
}

let instruction = AllowMintBuilder::new()
.payer(context.payer.pubkey())
.admin(admin.pubkey())
.instance(instance_pda)
.mint(mint.pubkey())
.allowed_mint(allowed_mint_pda)
.instance_ata(instance_ata)
.system_program(SYSTEM_PROGRAM_ID)
.token_program(TOKEN_2022_PROGRAM_ID)
.associated_token_program(spl_associated_token_account::ID)
.event_authority(event_authority_pda)
.contra_escrow_program(CONTRA_ESCROW_PROGRAM_ID)
.bump(bump)
.instruction();
#[test]
fn test_allow_mint_token_2022_pausable_accepted() {
let mut context = TestContext::new();
let admin = Keypair::new();
let mint = Keypair::new();
let authority = Keypair::new();

let result = context.send_transaction_with_signers(instruction, &[&admin]);
let instance_seed = Keypair::new();

set_mint_2022_with_pausable(&mut context, &mint.pubkey(), &authority.pubkey());

let (instance_pda, _) =
assert_get_or_create_instance(&mut context, &admin, &instance_seed, false, false)
.expect("CreateInstance should succeed");

assert_program_error(result, PERMANENT_DELEGATE_NOT_ALLOWED_ERROR);
assert_get_or_allow_mint(
&mut context,
&admin,
&instance_pda,
&mint.pubkey(),
false,
true,
)
.expect("AllowMint should succeed for a pausable Token-2022 mint");
}

#[test]
fn test_allow_mint_token_2022_pausable_blocked() {
fn test_allow_mint_token_2022_transfer_hook_blocked() {
let mut context = TestContext::new();
let admin = Keypair::new();
let mint = Keypair::new();
let authority = Keypair::new();
// Hook program id is arbitrary — the check fires on the extension's
// presence, not on whether the hook program exists on-chain.
let hook_program_id = Pubkey::new_unique();

let instance_seed = Keypair::new();

set_mint_2022_with_pausable(&mut context, &mint.pubkey(), &authority.pubkey());
set_mint_2022_with_transfer_hook(&mut context, &mint.pubkey(), &hook_program_id);

let (instance_pda, _) =
assert_get_or_create_instance(&mut context, &admin, &instance_seed, false, false)
Expand Down Expand Up @@ -410,5 +417,5 @@ fn test_allow_mint_token_2022_pausable_blocked() {

let result = context.send_transaction_with_signers(instruction, &[&admin]);

assert_program_error(result, PAUSABLE_MINT_NOT_ALLOWED_ERROR);
assert_program_error(result, TRANSFER_HOOK_NOT_ALLOWED_ERROR);
}
Loading
Loading