Skip to content
Draft
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
8 changes: 4 additions & 4 deletions idl/escrow_program.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
{
"defaultValue": {
"kind": "numberValueNode",
"number": 3
"number": 4
},
"kind": "structFieldTypeNode",
"name": "discriminator",
Expand Down Expand Up @@ -103,7 +103,7 @@
{
"defaultValue": {
"kind": "numberValueNode",
"number": 0
"number": 1
},
"kind": "structFieldTypeNode",
"name": "discriminator",
Expand Down Expand Up @@ -159,7 +159,7 @@
{
"defaultValue": {
"kind": "numberValueNode",
"number": 1
"number": 2
},
"kind": "structFieldTypeNode",
"name": "discriminator",
Expand Down Expand Up @@ -210,7 +210,7 @@
{
"defaultValue": {
"kind": "numberValueNode",
"number": 2
"number": 3
},
"kind": "structFieldTypeNode",
"name": "discriminator",
Expand Down
18 changes: 10 additions & 8 deletions program/src/instructions/block_mint/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use pinocchio::{account::AccountView, Address, ProgramResult};
use crate::{
events::BlockMintEvent,
instructions::BlockMint,
state::{AllowedMint, AllowedMintPda, Escrow},
traits::{EventSerialize, PdaSeeds},
state::{AllowedMint, Escrow},
traits::EventSerialize,
utils::{close_pda_account, emit_event},
};

Expand All @@ -19,13 +19,15 @@ pub fn process_block_mint(program_id: &Address, accounts: &[AccountView], instru
let escrow = Escrow::from_account(&escrow_data, ix.accounts.escrow, program_id)?;
escrow.validate_admin(ix.accounts.admin.address())?;

// Verify allowed_mint exists and is valid
// Verify allowed_mint account exists and self-validates against escrow + mint PDA derivation
let allowed_mint_data = ix.accounts.allowed_mint.try_borrow()?;
let allowed_mint = AllowedMint::from_account(&allowed_mint_data)?;

// Validate that allowed_mint PDA matches the escrow + mint combination
let pda_seeds = AllowedMintPda::new(ix.accounts.escrow.address(), ix.accounts.mint.address());
pda_seeds.validate_pda(ix.accounts.allowed_mint, program_id, allowed_mint.bump)?;
let _allowed_mint = AllowedMint::from_account(
&allowed_mint_data,
ix.accounts.allowed_mint,
program_id,
ix.accounts.escrow.address(),
ix.accounts.mint.address(),
)?;
drop(allowed_mint_data);

// Close the AllowedMint account and return lamports to rent_recipient
Expand Down
5 changes: 3 additions & 2 deletions program/src/instructions/deposit/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{
traits::InstructionAccounts,
utils::{
validate_associated_token_account, verify_current_program, verify_current_program_account,
verify_event_authority, verify_readonly, verify_signer, verify_system_program, verify_token_program,
verify_writable,
verify_event_authority, verify_owned_by, verify_readonly, verify_signer, verify_system_program,
verify_token_program, verify_writable,
},
};

Expand Down Expand Up @@ -77,6 +77,7 @@ impl<'a> TryFrom<&'a [AccountView]> for DepositAccounts<'a> {

// 4. Validate program IDs
verify_token_program(token_program)?;
verify_owned_by(mint, token_program.address())?;
verify_system_program(system_program)?;
verify_current_program(escrow_program)?;
verify_event_authority(event_authority)?;
Expand Down
31 changes: 18 additions & 13 deletions program/src/instructions/deposit/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ use crate::{
events::DepositEvent,
instructions::Deposit,
state::{
get_extensions_from_account, validate_extensions_pda, AllowedMint, AllowedMintPda, Escrow, ExtensionType,
HookData, HookPoint, Receipt,
get_extensions_from_account, validate_extensions_pda, AllowedMint, Escrow, ExtensionType, HookData, HookPoint,
Receipt,
},
traits::{AccountSerialize, AccountSize, EventSerialize, ExtensionData, PdaSeeds},
utils::{create_pda_account, emit_event, get_mint_decimals},
utils::{create_pda_account, emit_event, get_mint_decimals, validate_mint_extensions},
};

/// Processes the Deposit instruction.
Expand All @@ -30,15 +30,16 @@ pub fn process_deposit(program_id: &Address, accounts: &[AccountView], instructi
let escrow_data = ix.accounts.escrow.try_borrow()?;
let _escrow = Escrow::from_account(&escrow_data, ix.accounts.escrow, program_id)?;

// Verify allowed_mint PDA exists and matches expected derivation
// Verify allowed_mint account exists and self-validates against escrow + mint PDA derivation
let allowed_mint_data = ix.accounts.allowed_mint.try_borrow()?;
let allowed_mint = AllowedMint::from_account(&allowed_mint_data).map_err(|_| EscrowProgramError::MintNotAllowed)?;

// Validate that the allowed_mint PDA is derived from the correct escrow + mint
let pda_seeds = AllowedMintPda::new(ix.accounts.escrow.address(), ix.accounts.mint.address());
pda_seeds
.validate_pda(ix.accounts.allowed_mint, program_id, allowed_mint.bump)
.map_err(|_| EscrowProgramError::MintNotAllowed)?;
let _allowed_mint = AllowedMint::from_account(
&allowed_mint_data,
ix.accounts.allowed_mint,
program_id,
ix.accounts.escrow.address(),
ix.accounts.mint.address(),
)
.map_err(|_| EscrowProgramError::MintNotAllowed)?;

// Get current timestamp from Clock sysvar
let clock = Clock::get()?;
Expand Down Expand Up @@ -74,6 +75,10 @@ pub fn process_deposit(program_id: &Address, accounts: &[AccountView], instructi
// Validate extensions PDA
validate_extensions_pda(ix.accounts.escrow, ix.accounts.extensions, program_id)?;

// Re-check mint extensions against the current escrow blocklist.
// This prevents stale AllowedMint entries from bypassing new blocklist rules.
validate_mint_extensions(ix.accounts.mint, ix.accounts.extensions)?;

// Get hook extension if present
let exts = get_extensions_from_account(ix.accounts.extensions, &[ExtensionType::Hook])?;
let hook_data = exts[0].as_ref().map(|b| HookData::from_bytes(b)).transpose()?;
Expand All @@ -83,7 +88,7 @@ pub fn process_deposit(program_id: &Address, accounts: &[AccountView], instructi
hook.invoke(
HookPoint::PreDeposit,
ix.accounts.remaining_accounts,
&[ix.accounts.escrow, ix.accounts.depositor, ix.accounts.mint, ix.accounts.receipt],
&[ix.accounts.escrow, ix.accounts.mint, ix.accounts.receipt],
)?;
}

Expand All @@ -106,7 +111,7 @@ pub fn process_deposit(program_id: &Address, accounts: &[AccountView], instructi
hook.invoke(
HookPoint::PostDeposit,
ix.accounts.remaining_accounts,
&[ix.accounts.escrow, ix.accounts.depositor, ix.accounts.mint, ix.accounts.receipt],
&[ix.accounts.escrow, ix.accounts.mint, ix.accounts.receipt],
)?;
}

Expand Down
17 changes: 8 additions & 9 deletions program/src/instructions/extensions/add_timelock/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use pinocchio::{account::AccountView, cpi::Seed, error::ProgramError, Address, P
use crate::{
events::TimelockAddedEvent,
instructions::AddTimelock,
state::{append_extension, Escrow, ExtensionType, ExtensionsPda, TimelockData},
traits::{EventSerialize, PdaSeeds},
utils::{emit_event, TlvWriter},
state::{update_or_append_extension, Escrow, ExtensionType, ExtensionsPda, TimelockData},
traits::{EventSerialize, ExtensionData, PdaSeeds},
utils::emit_event,
};

/// Processes the AddTimelock instruction.
Expand All @@ -24,23 +24,22 @@ pub fn process_add_timelock(program_id: &Address, accounts: &[AccountView], inst
let extensions_pda = ExtensionsPda::new(ix.accounts.escrow.address());
extensions_pda.validate_pda(ix.accounts.extensions, program_id, ix.data.extensions_bump)?;

// Build TLV data
// Build extension data
let timelock = TimelockData::new(ix.data.lock_duration);
let mut tlv_writer = TlvWriter::new();
tlv_writer.write_timelock(&timelock);
let timelock_bytes = timelock.to_bytes();

// Get seeds and append extension
// Get seeds and append/update extension
let extensions_bump_seed = [ix.data.extensions_bump];
let extensions_seeds: Vec<Seed> = extensions_pda.seeds_with_bump(&extensions_bump_seed);
let extensions_seeds_array: [Seed; 3] = extensions_seeds.try_into().map_err(|_| ProgramError::InvalidArgument)?;

append_extension(
update_or_append_extension(
ix.accounts.payer,
ix.accounts.extensions,
program_id,
ix.data.extensions_bump,
ExtensionType::Timelock,
&tlv_writer.into_bytes(),
&timelock_bytes,
extensions_seeds_array,
)?;

Expand Down
18 changes: 8 additions & 10 deletions program/src/instructions/extensions/set_arbiter/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ use pinocchio::{account::AccountView, cpi::Seed, error::ProgramError, Address, P
use crate::{
events::ArbiterSetEvent,
instructions::SetArbiter,
state::{append_extension, ArbiterData, Escrow, ExtensionType, ExtensionsPda},
traits::{EventSerialize, PdaSeeds},
utils::{emit_event, TlvWriter},
state::{update_or_append_extension, ArbiterData, Escrow, ExtensionType, ExtensionsPda},
traits::{EventSerialize, ExtensionData, PdaSeeds},
utils::emit_event,
};

/// Processes the SetArbiter instruction.
///
/// Sets the arbiter on an escrow. Creates extensions PDA if it doesn't exist.
/// The arbiter is immutable — this instruction will fail if an arbiter is already set.
pub fn process_set_arbiter(program_id: &Address, accounts: &[AccountView], instruction_data: &[u8]) -> ProgramResult {
let ix = SetArbiter::try_from((instruction_data, accounts))?;

Expand All @@ -25,23 +24,22 @@ pub fn process_set_arbiter(program_id: &Address, accounts: &[AccountView], instr
let extensions_pda = ExtensionsPda::new(ix.accounts.escrow.address());
extensions_pda.validate_pda(ix.accounts.extensions, program_id, ix.data.extensions_bump)?;

// Build TLV data
// Build extension data
let arbiter = ArbiterData::new(*ix.accounts.arbiter.address());
let mut tlv_writer = TlvWriter::new();
tlv_writer.write_arbiter(&arbiter);
let arbiter_bytes = arbiter.to_bytes();

// Get seeds and append extension (fails if arbiter already exists, enforcing immutability)
// Get seeds and append/update extension
let extensions_bump_seed = [ix.data.extensions_bump];
let extensions_seeds: Vec<Seed> = extensions_pda.seeds_with_bump(&extensions_bump_seed);
let extensions_seeds_array: [Seed; 3] = extensions_seeds.try_into().map_err(|_| ProgramError::InvalidArgument)?;

append_extension(
update_or_append_extension(
ix.accounts.payer,
ix.accounts.extensions,
program_id,
ix.data.extensions_bump,
ExtensionType::Arbiter,
&tlv_writer.into_bytes(),
&arbiter_bytes,
extensions_seeds_array,
)?;

Expand Down
17 changes: 8 additions & 9 deletions program/src/instructions/extensions/set_hook/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use pinocchio::{account::AccountView, cpi::Seed, error::ProgramError, Address, P
use crate::{
events::HookSetEvent,
instructions::SetHook,
state::{append_extension, Escrow, ExtensionType, ExtensionsPda, HookData},
traits::{EventSerialize, PdaSeeds},
utils::{emit_event, TlvWriter},
state::{update_or_append_extension, Escrow, ExtensionType, ExtensionsPda, HookData},
traits::{EventSerialize, ExtensionData, PdaSeeds},
utils::emit_event,
};

/// Processes the SetHook instruction.
Expand All @@ -24,23 +24,22 @@ pub fn process_set_hook(program_id: &Address, accounts: &[AccountView], instruct
let extensions_pda = ExtensionsPda::new(ix.accounts.escrow.address());
extensions_pda.validate_pda(ix.accounts.extensions, program_id, ix.data.extensions_bump)?;

// Build TLV data
// Build extension data
let hook = HookData::new(ix.data.hook_program);
let mut tlv_writer = TlvWriter::new();
tlv_writer.write_hook(&hook);
let hook_bytes = hook.to_bytes();

// Get seeds and append extension
// Get seeds and append/update extension
let extensions_bump_seed = [ix.data.extensions_bump];
let extensions_seeds: Vec<Seed> = extensions_pda.seeds_with_bump(&extensions_bump_seed);
let extensions_seeds_array: [Seed; 3] = extensions_seeds.try_into().map_err(|_| ProgramError::InvalidArgument)?;

append_extension(
update_or_append_extension(
ix.accounts.payer,
ix.accounts.extensions,
program_id,
ix.data.extensions_bump,
ExtensionType::Hook,
&tlv_writer.into_bytes(),
&hook_bytes,
extensions_seeds_array,
)?;

Expand Down
5 changes: 3 additions & 2 deletions program/src/instructions/withdraw/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{
traits::InstructionAccounts,
utils::{
validate_associated_token_account, verify_current_program, verify_current_program_account,
verify_event_authority, verify_readonly, verify_signer, verify_system_program, verify_token_program,
verify_writable,
verify_event_authority, verify_owned_by, verify_readonly, verify_signer, verify_system_program,
verify_token_program, verify_writable,
},
};

Expand Down Expand Up @@ -74,6 +74,7 @@ impl<'a> TryFrom<&'a [AccountView]> for WithdrawAccounts<'a> {

// 4. Validate program IDs
verify_token_program(token_program)?;
verify_owned_by(mint, token_program.address())?;
verify_system_program(system_program)?;
verify_current_program(escrow_program)?;
verify_event_authority(event_authority)?;
Expand Down
23 changes: 14 additions & 9 deletions program/src/instructions/withdraw/processor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use pinocchio::{account::AccountView, Address, ProgramResult};
use pinocchio::{account::AccountView, error::ProgramError, Address, ProgramResult};
use pinocchio_token_2022::instructions::TransferChecked;

use crate::{
Expand All @@ -25,7 +25,7 @@ pub fn process_withdraw(program_id: &Address, accounts: &[AccountView], instruct
}

// Read and validate receipt
let (amount, receipt_seed, mint, deposited_at) = {
let (amount, receipt_seed, receipt_mint, deposited_at) = {
let receipt_data = ix.accounts.receipt.try_borrow()?;
let receipt = Receipt::from_account(&receipt_data, ix.accounts.receipt, program_id)?;

Expand All @@ -35,6 +35,11 @@ pub fn process_withdraw(program_id: &Address, accounts: &[AccountView], instruct
(receipt.amount, receipt.receipt_seed, receipt.mint, receipt.deposited_at)
};

// Ensure the mint account matches the receipt's mint to prevent cross-mint withdrawals.
if receipt_mint != *ix.accounts.mint.address() {
return Err(ProgramError::InvalidAccountData);
}

// Validate extensions PDA
validate_extensions_pda(ix.accounts.escrow, ix.accounts.extensions, program_id)?;

Expand Down Expand Up @@ -68,7 +73,7 @@ pub fn process_withdraw(program_id: &Address, accounts: &[AccountView], instruct
hook.invoke(
HookPoint::PreWithdraw,
remaining_accounts,
&[ix.accounts.escrow, ix.accounts.withdrawer, ix.accounts.mint, ix.accounts.receipt],
&[ix.accounts.escrow, ix.accounts.mint, ix.accounts.receipt],
)?;
}

Expand All @@ -92,23 +97,23 @@ pub fn process_withdraw(program_id: &Address, accounts: &[AccountView], instruct
})?;
}

// Close receipt account and return lamports to rent_recipient
close_pda_account(ix.accounts.receipt, ix.accounts.rent_recipient)?;

// Invoke post-withdraw hook if configured (receipt is closed, don't pass it)
// Invoke post-withdraw hook if configured (receipt is still open, pass it for context)
if let Some(ref hook) = hook_data {
hook.invoke(
HookPoint::PostWithdraw,
remaining_accounts,
&[ix.accounts.escrow, ix.accounts.withdrawer, ix.accounts.mint],
&[ix.accounts.escrow, ix.accounts.mint, ix.accounts.receipt],
)?;
}

// Close receipt account and return lamports to rent_recipient
close_pda_account(ix.accounts.receipt, ix.accounts.rent_recipient)?;

// Emit event
let event = WithdrawEvent::new(
*ix.accounts.escrow.address(),
*ix.accounts.withdrawer.address(),
mint,
receipt_mint,
receipt_seed,
amount,
);
Expand Down
Loading
Loading