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.

Program Details

  • Program ID: CCSwaptcDXtfjyRMYBqavqBwiW162yXFAJrXN53UuENC
  • Framework: Anchor (Solana)
  • Blockchain: Solana
  • Currency: USDC (SPL Token)

Devnet USDC Faucet

Use this faucet to get the USDC token used on devnet: https://spl-token-faucet.com/?token-name=USDC-Dev

Overview

The swap protocol supports atomic, two-party exchanges across NFTs and tokens. Both parties deposit assets into program-controlled escrows, approve the trade, then execute and settle.

Supported Asset Types

  • Standard NFTs: SPL token-based NFTs
  • Programmable NFTs (pNFTs): Token Metadata standard with enforced royalties
  • Compressed NFTs (cNFTs): Merkle-tree-based assets via Metaplex Bubblegum
  • USDC Tokens: SPL token transfers

Key Features

  • Escrow-based custody: Assets are held in PDA escrows during trade lifecycle
  • Multi-asset support: NFTs, pNFTs, cNFTs, and USDC can be combined in one trade
  • Mutual consent flow: Both users must approve and execute before settlement
  • Deterministic trade identity: Trade uniqueness enforced via per-pair nonce
  • Backend co-signer support: Embedded wallet and sponsored transaction workflows
Escrow-Based System - Settlement Responsibility

Assets are transferred into escrow once deposited. A trade only completes when both users approve and execute. Either user can cancel and withdraw before final execution.

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, USDC) into their escrows
  2. Assets remain in escrow pending approval and execution

Approval Flow

  1. User1 calls approve_tradeuser1_approved = true
  2. User2 calls approve_tradeuser2_approved = true
  3. Trade status transitions to Approved

Execution Flow

  1. User1 calls execute_tradeuser1_executed = true
  2. User2 calls execute_tradeuser2_executed = true
  3. Trade status transitions to Executed
  4. Assets are settled through batch execution instructions
  5. Trade and escrow PDAs are closed via close_trade

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

Account Structure

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
ExecutedExecution initiated; assets can be transferred
CancelledTrade was cancelled; assets can be withdrawn

UserEscrow

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

FieldTypeDescription
bumpu8PDA bump seed

Program Instructions

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

Signers: authority

Parameters:

  • nonce: u64

Key Accounts:

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

Preconditions:

  • Trade status is Executed 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.

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_token_batch

Purpose: Transfer USDC 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)
  • usdc_mint
  • source_escrow_usdc_ata (mut)
  • user1_usdc_ata (mut, init_if_needed)
  • user2_usdc_ata (mut, init_if_needed)
  • token_program, associated_token_program, system_program

Preconditions:

  • Trade status is Executed
  • All USDC in escrow is 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]

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.

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.

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.

withdraw_token

Purpose: Withdraw USDC 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)
  • usdc_mint
  • source_escrow_usdc_ata (mut)
  • user_usdc_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.

Constants

ConstantValueDescription
MAX_ASSET_SIZE65 bytesMaximum asset size
USER_ESCROW_LEN9 bytesAccount size for UserEscrow struct
RENT_THRESHOLD_LAMPORTS5,000,000Rent refund threshold; if user is below threshold, refund may go to backend

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. close_trade(nonce)
-> Trade and escrow PDAs closed

Flow 2: Mixed Asset Swap (NFT + USDC)

1. User1 calls create_trade(nonce)
2. User1 deposits NFT(s); User2 deposits USDC
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 USDC to User1
7. close_trade performs 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 USDC
5. close_trade closes PDAs and refunds 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, PROGRAM_ID, provider);

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,
// ... include required program state accounts from your IDL
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 USDC mint
6012TradingPausedTrading is temporarily paused
6013UserNotApprovedUser has not approved before execution

Common Failure Scenarios

ScenarioErrorResolution
Trading pausedTradingPausedRetry when trading is resumed
Approving twiceTradeAlreadyApprovedAlready approved; no action needed
Executing before approvalUserNotApprovedCall approve_trade first
Executing twiceTradeAlreadyExecutedAlready executed; no action needed
Withdrawing after executionCannotWithdrawInCurrentStatusCannot withdraw after execution
Wrong trade statusInvalidTradeStatusConfirm status and use valid instruction
Insufficient USDC withdrawal amountInsufficientFundsReduce withdrawal amount
Wrong USDC mintInvalidMintUse the program's supported USDC 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

USDC Mint Validation

All USDC operations validate mint address against the program USDC mint to prevent token substitution.

Rent Handling

  • Trade creator is stored in trade.creator
  • close_trade returns rent to creator
  • If user balance is below RENT_THRESHOLD_LAMPORTS (5,000,000 lamports), rent may route to backend to avoid failed settlement

External Program ID Validation

All external program accounts are constrained to expected IDs:

  • MPL Token Metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
  • Metaplex Bubblegum: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY
  • 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, and settlement are separate transactions, increasing UX complexity in exchange for explicit mutual consent.
Rent threshold behaviorLow-balance users may have rent redirected to backend to reduce close-time failure risk.

External Program Dependencies

ProgramIDUsage
MPL Token MetadatametaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1spNFT transfer and token record management
Metaplex BubblegumBGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUYcNFT 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.