Skip to main content

Swap Program

The CollectorCrypt Swap Program is a Solana smart contract for peer-to-peer asset swaps using escrow-based settlement. For a high-level introduction and supported asset types, see the Swap Overview.

Program Details​

  • Program ID: CCSwaptcDXtfjyRMYBqavqBwiW162yXFAJrXN53UuENC
  • Framework: Anchor (Solana)
  • Blockchain: Solana
  • Currency: any legacy SPL Token mint (the app currently offers USDC, USDT and CARDS)

Architecture​

Transaction Flows​

Trade Creation Flow​

  1. User calls create_trade with counterparty and nonce
  2. Trade PDA is created with status Open
  3. User1 and User2 escrow PDAs are created

Asset Deposit Flow​

  1. Users transfer assets (NFTs, pNFTs, cNFTs, SPL tokens) into their escrows
  2. Assets remain in escrow pending approval and execution

Approval Flow​

  1. User1 calls approve_trade → user1_approved = true
  2. User2 calls approve_trade → user2_approved = true
  3. Trade status transitions to Approved

Execution Flow​

  1. User1 calls execute_trade → user1_executed = true
  2. User2 calls execute_trade → user2_executed = true
  3. Trade status transitions to Executed
  4. Assets are settled through batch execution instructions
  5. Backend calls mark_trade_settled → status transitions to Settled
  6. Trade and escrow PDAs are closed via close_trade (backend only)

Cancellation Flow​

  1. Either user calls cancel_trade (status must be Open or Approved)
  2. Trade status transitions to Cancelled
  3. Users withdraw assets via withdraw instructions
  4. Trade and escrows are closed via close_trade (backend only)

Account Structure​

Config​

PDA Seeds: ["config"] (singleton)

FieldTypeDescription
config_adminPubkeyAdmin authority for config changes and emergency actions
backend_accountPubkeyBackend co-signer authorized to settle and close trades
spl_token_mintPubkeyThe SPL token accepted in trades
trading_pausedboolGlobal pause flag; blocks new trades and executions when set
config_bump[u8; 1]PDA bump seed

Trade​

PDA Seeds: ["trade", user1, user2, nonce_bytes]

FieldTypeDescription
user1PubkeyFirst user in the trade
user2PubkeySecond user in the trade
nonceu64Unique nonce for this trade pair
creatorPubkeyAccount that paid for trade creation
user1_approvedboolUser1 approval status
user2_approvedboolUser2 approval status
user1_executedboolUser1 execution completion flag
user2_executedboolUser2 execution completion flag
trade_statusTradeStatusCurrent trade status
bumpu8PDA bump seed

TradeStatus​

VariantDescription
OpenAssets can be added/withdrawn, approvals can be set
ApprovedBoth users approved; can execute, withdraw (resets approvals), or cancel
ExecutedBoth users executed; assets can be transferred via batch instructions
CancelledTrade was cancelled; assets can be withdrawn, then close_trade closes PDAs
SettledBackend marked trade settled; only close_trade or force_cancel_settled_trade allowed

Borsh discriminants, for mirroring the enum elsewhere: Open = 0, Approved = 1, Executed = 2, Cancelled = 3, Settled = 4.

UserEscrow​

PDA Seeds: ["escrow", user1, user2, nonce_bytes, "user1"] (or "user2" for user2 escrow)
Size: 9 bytes

FieldTypeDescription
bumpu8PDA bump seed

Program Instructions​

Configuration Instructions​

Admin-only setup and controls acting on the Config singleton. initialize runs once; the setters are gated on config.config_admin.

InstructionSignerPurpose
initialize(config_admin, backend_account, spl_token_mint)Program upgrade authorityCreate the Config PDA and set the admin, backend co-signer, and trade token. Callable once.
set_config_admin(config_admin)config_adminRotate the config admin
set_backend_account(backend_account)config_adminRotate the backend co-signer
set_trading_paused(paused)config_adminPause or resume trading — while paused, create_trade, approve_trade, unapprove_trade, execute_trade, cancel_trade, close_trade, mark_trade_settled and the execute-batch instructions all revert with TradingPaused. The withdraw_* instructions are deliberately left open so assets can always be recovered while paused

Trade Management Instructions​

create_trade​

Purpose: Create a new trade between two users

Signers: authority, payer

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — must be user1 or user2
  • backend (mut) — backend account for validation
  • payer (mut, signer) — must be authority or backend
  • user1, user2
  • trade (init PDA)
  • user1_escrow (init PDA)
  • user2_escrow (init PDA)
  • system_program

Preconditions:

  • Authority is user1 or user2
  • Payer is authority or backend signer

State: Creates Trade PDA (Open) and both escrow PDAs.

Events Emitted: TradeCreated

approve_trade​

Purpose: Approve trade readiness

Signers: authority

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — must be user1 or user2
  • user1, user2
  • trade (mut)

Preconditions:

  • Trade status is Open
  • Authority is user1 or user2
  • Caller has not already approved

State: Sets caller approval flag. If both are approved, status moves to Approved.

unapprove_trade​

Purpose: Reset trade from Approved/Open state back to Open

Signers: authority

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — must be user1 or user2
  • user1, user2
  • trade (mut)

Preconditions:

  • Trade status is Open or Approved
  • Authority is user1 or user2

State: Resets approval/execution flags and returns status to Open.

cancel_trade​

Purpose: Cancel an open or approved trade

Signers: authority

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — must be user1 or user2
  • user1, user2
  • trade (mut)

Preconditions:

  • Trade status is Open or Approved
  • Caller is user1 or user2

State: Trade status transitions to Cancelled. Assets are withdrawn separately.

close_trade​

Purpose: Close a completed trade and reclaim rent (backend only)

Signers: backend

Parameters:

  • nonce: u64

Key Accounts:

  • backend (signer) — must be config.backend_account
  • user1, user2
  • creator (mut) — original trade creator (rent refund recipient)
  • trade (mut, close)
  • user1_escrow (mut, close)
  • user2_escrow (mut, close)

Preconditions:

  • Signer is config.backend_account
  • Trade status is Settled or Cancelled

State: Closes trade and escrow PDAs. Rent is refunded to creator.


Execution Instructions​

execute_trade​

Purpose: Signal execution commitment by each user

Signers: authority

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — must be user1 or user2
  • user1, user2
  • trade (mut)

Preconditions:

  • Trade status is Approved
  • Caller has not already executed

State: Sets caller execution flag. If both are set, status moves to Executed.

mark_trade_settled​

Purpose: Backend marks a trade as settled after all asset transfers are complete

Signers: backend

Parameters:

  • nonce: u64

Key Accounts:

  • backend (signer) — must be config.backend_account
  • user1, user2
  • trade (mut)

Preconditions:

  • Signer is config.backend_account
  • Trade status is Executed

State: Trade status transitions to Settled. This is the final step before close_trade.

execute_nft_batch​

Purpose: Transfer standard NFTs from escrow to recipient

Signers: authority, payer

Parameters:

  • nonce: u64
  • nft_mints: Vec<Pubkey>

Key Accounts:

  • authority (mut, signer) — user1, user2, or backend
  • payer (mut, signer)
  • user1 (mut), user2 (mut)
  • backend (mut)
  • trade (mut)
  • source_escrow (mut)
  • token_program, associated_token_program, system_program
  • Remaining Accounts (per NFT): [from_ata, to_ata, mint_account]

Preconditions:

  • Trade status is Executed
  • Authority is user1, user2, or backend

execute_pnft_batch​

Purpose: Transfer programmable NFTs from escrow to recipient

Signers: authority, payer

Parameters:

  • nonce: u64
  • pnft_mints: Vec<Pubkey>

Key Accounts:

  • authority (mut, signer) — user1, user2, or backend
  • payer (mut, signer)
  • user1 (mut), user2 (mut)
  • backend (mut)
  • trade (mut)
  • source_escrow (mut)
  • token_program, associated_token_program, system_program
  • token_metadata_program, sysvar_instructions, auth_rules_program
  • Remaining Accounts (per pNFT): [mint, from_ata, to_ata, metadata, edition, from_token_record, to_token_record, authorization_rules?, authorization_rules_program?]

Preconditions:

  • Trade status is Executed
  • Authority is user1, user2, or backend

execute_cnft_batch​

Purpose: Transfer compressed NFTs from escrow to recipient

Signers: authority

Parameters:

  • nonce: u64
  • cnft_data: Vec<CnftTransferData>

Key Accounts:

  • authority (mut, signer) — user1, user2, or backend
  • user1 (mut), user2 (mut)
  • trade (mut)
  • source_escrow (mut)
  • bubblegum_program, compression_program, log_wrapper, system_program
  • Remaining Accounts (per cNFT): [tree_authority, merkle_tree, core_collection?, ...proof_path_accounts]

CnftTransferData Structure:

CnftTransferData {
pub root: [u8; 32],
pub data_hash: [u8; 32],
pub creator_hash: [u8; 32],
pub nonce: u64,
pub index: u32,
pub asset_data_hash: Option<[u8; 32]>,
pub flags: Option<u8>,
}

execute_core_batch​

Purpose: Transfer Metaplex Core assets from escrow to recipient

Signers: authority, payer

Parameters:

  • nonce: u64
  • core_data: Vec<CoreTransferData>

Key Accounts:

  • authority (mut, signer) — user1, user2, or backend
  • payer (mut, signer)
  • config (enforces not paused)
  • user1 (mut), user2 (mut)
  • trade (mut)
  • source_escrow (mut)
  • mpl_core_program (address = MPL Core), system_program
  • Remaining Accounts (per asset): [asset, collection?] — collection present only when has_collection is set

Preconditions:

  • Trade status is Executed
  • Authority is user1, user2, or backend

CoreTransferData Structure:

CoreTransferData {
pub asset: Pubkey,
pub has_collection: bool,
}

execute_token_batch​

Purpose: Transfer SPL tokens from escrow to recipient

Signers: authority, payer

Parameters:

  • nonce: u64

Key Accounts:

  • authority (mut, signer) — user1, user2, or backend
  • payer (mut, signer)
  • user1 (mut), user2 (mut)
  • backend (mut)
  • trade (mut)
  • source_escrow (mut)
  • spl_token_mint — the SPL mint being moved; the escrow and user accounts must be this mint's canonical ATAs
  • source_escrow_spl_token_ata (mut)
  • user1_spl_token_ata (mut, init_if_needed)
  • user2_spl_token_ata (mut, init_if_needed)
  • token_program, associated_token_program, system_program

Preconditions:

  • Trade status is Executed
  • All tokens in escrow are transferred to recipient side

Withdrawal Instructions​

withdraw_nft​

Purpose: Withdraw a standard NFT from escrow

Signers: authority, payer

Parameters:

  • nonce: u64
  • nft_mint: Pubkey

Key Accounts:

  • authority (mut, signer) — NFT owner (user1 or user2)
  • payer (mut, signer)
  • backend (mut)
  • user1 (mut), user2 (mut)
  • trade (mut)
  • source_escrow (mut)
  • token_program, associated_token_program, system_program
  • Remaining Accounts: [from_ata, to_ata, mint_account] — mint_account must equal the trade's nft_mint or the call fails MintMismatch (6004)

Preconditions:

  • Trade status is Open, Approved, or Cancelled
  • Caller owns the escrow being withdrawn from

State: If status is not Cancelled, approvals/execution are reset to Open.

withdraw_pnft​

Purpose: Withdraw a programmable NFT from escrow

Signers: authority, payer

Parameters:

  • nonce: u64
  • pnft_mint: Pubkey

Key Accounts:

  • authority (mut, signer) — pNFT owner
  • payer (mut, signer)
  • backend (mut)
  • user1 (mut), user2 (mut)
  • trade (mut)
  • source_escrow (mut)
  • token_program, associated_token_program, system_program
  • token_metadata_program, sysvar_instructions, auth_rules_program
  • Remaining Accounts: [mint, from_ata, to_ata, metadata, edition, from_token_record, to_token_record, authorization_rules?]

Preconditions:

  • Trade status is Open, Approved, or Cancelled

State: If status is not Cancelled, approvals/execution are reset to Open.

withdraw_cnft​

Purpose: Withdraw a compressed NFT from escrow

Signers: authority, payer

Parameters:

  • nonce: u64
  • cnft_data: CnftTransferData

Key Accounts:

  • authority (mut, signer) — cNFT owner
  • payer (mut, signer)
  • trade (mut)
  • source_escrow (mut)
  • bubblegum_program, compression_program, log_wrapper, system_program
  • Remaining Accounts: [tree_authority, merkle_tree, core_collection?, ...proof_path_accounts]

Preconditions:

  • Trade status is Open, Approved, or Cancelled

State: If status is not Cancelled, approvals/execution are reset to Open.

withdraw_token​

Purpose: Withdraw SPL tokens from escrow

Signers: authority, payer

Parameters:

  • nonce: u64
  • amount: u64

Key Accounts:

  • authority (mut, signer) — token owner
  • payer (mut, signer)
  • backend (mut)
  • user1 (mut), user2 (mut)
  • trade (mut)
  • source_escrow (mut)
  • spl_token_mint — the SPL mint being moved; the escrow and user accounts must be this mint's canonical ATAs
  • source_escrow_spl_token_ata (mut)
  • user_spl_token_ata (mut, init_if_needed)
  • token_program, associated_token_program, system_program

Preconditions:

  • Trade status is Open, Approved, or Cancelled
  • amount > 0 and amount <= available_balance

State: If status is not Cancelled, approvals/execution are reset to Open.

withdraw_core​

Purpose: Withdraw a Metaplex Core asset from escrow

Signers: authority, payer

Parameters:

  • nonce: u64
  • core_data: CoreTransferData

Key Accounts:

  • authority (mut, signer) — asset owner
  • payer (mut, signer)
  • trade (mut)
  • source_escrow (mut)
  • mpl_core_program (address = MPL Core), system_program
  • Remaining Accounts: [asset, collection?]

Preconditions:

  • Trade status is Open, Approved, or Cancelled

State: If status is not Cancelled, approvals/execution are reset to Open.


Admin Emergency Instructions​

emergency_cancel_trade​

Purpose: Admin force-cancels any active (pre-settlement) trade

Signers: config_admin

Parameters:

  • nonce: u64

Key Accounts:

  • config_admin (signer) — must be config.config_admin
  • config
  • user1, user2
  • trade (mut)

Preconditions:

  • Signer is config.config_admin
  • Trade status is not Settled

State: Trade status transitions to Cancelled. All approval/execution flags are reset. Users can then withdraw and backend can close.

Use Case

For emergency intervention on active trades — e.g., dispute resolution or fraudulent activity detected before settlement.

force_cancel_settled_trade​

Purpose: Admin force-cancels a trade that has already been marked settled

Signers: config_admin

Parameters:

  • nonce: u64

Key Accounts:

  • config_admin (signer) — must be config.config_admin
  • config
  • user1, user2
  • trade (mut)

Preconditions:

  • Signer is config.config_admin
  • Trade status is exactly Settled

State: Trade status transitions from Settled to Cancelled. All approval/execution flags are reset.

Use Case

For exceptional post-settlement reversals — e.g., a settlement was confirmed on-chain but a dispute requires reversal before close_trade is called.

Constants​

ConstantValueDescription
MAX_ASSET_SIZE65 bytesMaximum serialized asset entry size
USER_ESCROW_LEN9 bytesAccount size for UserEscrow struct
CONFIG_LEN106 bytesAccount size for Config struct

Integration Flows​

Flow 1: Standard NFT Swap​

1. User1 calls create_trade(nonce)
-> Trade PDA created (Open)
-> User1 and User2 escrow PDAs created

2. Users deposit NFTs into respective escrows

3. User1 calls approve_trade(nonce)
-> user1_approved = true

4. User2 calls approve_trade(nonce)
-> user2_approved = true
-> Trade status: Approved

5. User1 calls execute_trade(nonce)
-> user1_executed = true

6. User2 calls execute_trade(nonce)
-> user2_executed = true
-> Trade status: Executed

7. execute_nft_batch(nonce, nft_mints)
-> NFTs transferred from escrows to recipients

8. Backend calls mark_trade_settled(nonce)
-> Trade status: Settled

9. Backend calls close_trade(nonce)
-> Trade and escrow PDAs closed, rent returned to creator

Flow 2: Mixed Asset Swap (NFT + SPL Token)​

1. User1 calls create_trade(nonce)
2. User1 deposits NFT(s); User2 deposits SPL tokens
3. Both users approve via approve_trade
4. Both users execute via execute_trade
5. execute_nft_batch moves NFT(s) to User2
6. execute_token_batch moves tokens to User1
7. Backend calls mark_trade_settled
8. Backend calls close_trade for final cleanup

Flow 3: Cancel and Withdraw​

1. Trade is created and assets are deposited
2. Either user calls cancel_trade(nonce)
-> Trade status: Cancelled
3. User1 withdraws NFT(s)
4. User2 withdraws SPL tokens
5. Backend calls close_trade, closing PDAs and refunding rent

Using Anchor Client​

import { BN, Program, AnchorProvider } from '@coral-xyz/anchor';
import { Connection, SystemProgram } from '@solana/web3.js';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const provider = new AnchorProvider(connection, wallet, {});
const program = new Program(IDL, provider); // anchor 0.31: the address comes from IDL.address

const nonce = new BN(Date.now());
await program.methods
.createTrade(nonce)
.accounts({
authority: user1.publicKey,
backend: backendPubkey,
payer: user1.publicKey,
user1: user1.publicKey,
user2: user2.publicKey,
trade: tradePda,
user1Escrow: user1EscrowPda,
user2Escrow: user2EscrowPda,
systemProgram: SystemProgram.programId,
})
.signers([user1])
.rpc();

PDA Derivation Examples​

// Trade PDA
const nonceBuf = Buffer.alloc(8);
nonceBuf.writeBigUInt64LE(BigInt(nonce));
const [tradePda] = PublicKey.findProgramAddressSync(
[Buffer.from('trade'), user1.toBuffer(), user2.toBuffer(), nonceBuf],
PROGRAM_ID
);

// User1 Escrow PDA
const [user1EscrowPda] = PublicKey.findProgramAddressSync(
[Buffer.from('escrow'), user1.toBuffer(), user2.toBuffer(), nonceBuf, Buffer.from('user1')],
PROGRAM_ID
);

// User2 Escrow PDA
const [user2EscrowPda] = PublicKey.findProgramAddressSync(
[Buffer.from('escrow'), user1.toBuffer(), user2.toBuffer(), nonceBuf, Buffer.from('user2')],
PROGRAM_ID
);

Events​

TradeCreated​

Emitted when a new trade is created via create_trade.

FieldTypeDescription
user1PubkeyFirst user in the trade
user2PubkeySecond user in the trade
nonceu64Trade nonce
creatorPubkeyAccount that created the trade
tradePubkeyTrade account address

Error Reference​

CodeNameDescription
6000UnauthorizedAccountSigner does not match required authority
6001TradeAlreadyApprovedUser has already approved this trade
6002InvalidEscrowAccountEscrow account does not match expected PDA
6003InvalidTokenAccountToken account does not match expected ATA
6004MintMismatchNFT mint does not match instruction data
6005NoAssetsTransferredEmpty asset list provided
6006CannotWithdrawInCurrentStatusTrade status does not allow withdrawals
6007InvalidTradeStatusTrade status does not support the operation
6008TradeAlreadyExecutedUser has already executed this trade
6009InsufficientFundsRequested amount exceeds available balance
6010InvalidRentDestinationRent destination account is invalid
6011InvalidMintMint address does not match program SPL token mint
6012TradingPausedTrading is temporarily paused
6013UserNotApprovedUser has not approved before execution
6014InvalidAssetAsset account is invalid — wrong owner or not a Metaplex Core asset

Common Failure Scenarios​

ScenarioErrorResolution
Trading pausedTradingPausedRetry when trading is resumed
Approving twiceTradeAlreadyApprovedAlready approved; no action needed
Executing before both approvalsInvalidTradeStatusTrade status must be Approved — both parties must approve_trade first. (UserNotApproved is declared but never returned by the current program.)
Executing twiceTradeAlreadyExecutedAlready executed; no action needed
Withdrawing after executionCannotWithdrawInCurrentStatusCannot withdraw after execution
Wrong trade statusInvalidTradeStatusConfirm status and use valid instruction
Insufficient token withdrawal amountInsufficientFundsReduce withdrawal amount
Wrong SPL token mintInvalidMintUse the program's configured SPL token mint

Security Considerations​

PDA Protections​

  • Deterministic derivation: All PDAs use fixed, reproducible seeds
  • Bump persistence: Bumps are stored on-chain for reliable re-derivation
  • Escrow authority isolation: Transfers require program-signed PDA authority

Mutual Approval and Execution​

Both approve_trade and execute_trade require independent user consent:

  • Trade cannot become Approved without both approvals
  • Trade cannot become Executed without both execution flags
  • Either party can cancel before execution finalization

SPL Token Mint Validation​

Token operations are not pinned to a single configured mint. spl_token_mint is unconstrained; substitution is prevented instead by requiring every token account to be the canonical ATA of whatever mint is passed, so assets cannot be redirected to a different mint's account.

Rent Handling​

  • Trade creator is stored in trade.creator
  • close_trade refunds trade and escrow rent to the creator

External Program ID Validation​

All external program accounts are constrained to expected IDs:

  • MPL Token Metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
  • Metaplex Bubblegum: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY
  • MPL Core: CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d
  • SPL Account Compression: mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW
  • SPL Noop: mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3
  • MPL Token Auth Rules: auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg

Known Tradeoffs​

TradeoffDescription
Escrow-based custodyAssets move into escrow during trade lifecycle. This improves deterministic settlement but adds explicit deposit/withdraw steps.
Nonce-based uniquenessConcurrent trades between the same users are supported, but each trade requires unique nonce management.
Multi-step executionApproval, execution, settlement, and closure are separate transactions, increasing UX complexity in exchange for explicit mutual consent and backend-controlled settlement.
Backend-only closeOnly backend_account can call close_trade, ensuring controlled cleanup after settlement confirmation.

External Program Dependencies​

ProgramIDUsage
MPL Token MetadatametaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1spNFT transfer and token record management
Metaplex BubblegumBGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUYcNFT transfers
MPL CoreCoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7dMetaplex Core asset transfers
SPL Account Compressionmcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAWMerkle tree verification
SPL NoopmnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3Log wrapper for Bubblegum
MPL Token Auth Rulesauth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmggpNFT authorization rules
SPL Token(standard)Token transfers
SPL Associated Token(standard)ATA creation/resolution
System Program(standard)Account creation and rent

Support​

For technical questions, integration support, or issue reporting, contact the CollectorCrypt development team.