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
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
- User calls
create_tradewith counterparty and nonce - Trade PDA is created with status
Open - User1 and User2 escrow PDAs are created
Asset Deposit Flow
- Users transfer assets (NFTs, pNFTs, cNFTs, USDC) into their escrows
- Assets remain in escrow pending approval and execution
Approval Flow
- User1 calls
approve_trade→user1_approved = true - User2 calls
approve_trade→user2_approved = true - Trade status transitions to
Approved
Execution Flow
- User1 calls
execute_trade→user1_executed = true - User2 calls
execute_trade→user2_executed = true - Trade status transitions to
Executed - Assets are settled through batch execution instructions
- Trade and escrow PDAs are closed via
close_trade
Cancellation Flow
- Either user calls
cancel_trade(status must beOpenorApproved) - Trade status transitions to
Cancelled - Users withdraw assets via withdraw instructions
- Trade and escrows are closed via
close_trade
Account Structure
Trade
PDA Seeds: ["trade", user1, user2, nonce_bytes]
| Field | Type | Description |
|---|---|---|
| user1 | Pubkey | First user in the trade |
| user2 | Pubkey | Second user in the trade |
| nonce | u64 | Unique nonce for this trade pair |
| creator | Pubkey | Account that paid for trade creation |
| user1_approved | bool | User1 approval status |
| user2_approved | bool | User2 approval status |
| user1_executed | bool | User1 execution completion flag |
| user2_executed | bool | User2 execution completion flag |
| trade_status | TradeStatus | Current trade status |
| bump | u8 | PDA bump seed |
TradeStatus
| Variant | Description |
|---|---|
| Open | Assets can be added/withdrawn, approvals can be set |
| Approved | Both users approved; can execute, withdraw (resets approvals), or cancel |
| Executed | Execution initiated; assets can be transferred |
| Cancelled | Trade was cancelled; assets can be withdrawn |
UserEscrow
PDA Seeds: ["escrow", user1, user2, nonce_bytes, "user1"] (or "user2" for user2 escrow)
Size: 9 bytes
| Field | Type | Description |
|---|---|---|
| bump | u8 | PDA 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 user2backend(mut) — backend account for validationpayer(mut, signer) — must be authority or backenduser1,user2trade(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 user2user1,user2trade(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 user2user1,user2trade(mut)
Preconditions:
- Trade status is
OpenorApproved - 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 user2user1,user2trade(mut)
Preconditions:
- Trade status is
OpenorApproved - 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 backenduser1,user2creator(mut) — original trade creator (rent refund recipient)trade(mut, close)user1_escrow(mut, close)user2_escrow(mut, close)
Preconditions:
- Trade status is
ExecutedorCancelled
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 user2user1,user2trade(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: u64nft_mints: Vec<Pubkey>
Key Accounts:
authority(mut, signer) — user1, user2, or backendpayer(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: u64pnft_mints: Vec<Pubkey>
Key Accounts:
authority(mut, signer) — user1, user2, or backendpayer(mut, signer)user1(mut),user2(mut)backend(mut)trade(mut)source_escrow(mut)token_program,associated_token_program,system_programtoken_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: u64cnft_data: Vec<CnftTransferData>
Key Accounts:
authority(mut, signer) — user1, user2, or backenduser1(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 backendpayer(mut, signer)user1(mut),user2(mut)backend(mut)trade(mut)source_escrow(mut)usdc_mintsource_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: u64nft_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, orCancelled - 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: u64pnft_mint: Pubkey
Key Accounts:
authority(mut, signer) — pNFT ownerpayer(mut, signer)backend(mut)user1(mut),user2(mut)trade(mut)source_escrow(mut)token_program,associated_token_program,system_programtoken_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, orCancelled
State: If status is not Cancelled, approvals/execution are reset.
withdraw_cnft
Purpose: Withdraw a compressed NFT from escrow
Signers: authority, payer
Parameters:
nonce: u64cnft_data: CnftTransferData
Key Accounts:
authority(mut, signer) — cNFT ownerpayer(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, orCancelled
State: If status is not Cancelled, approvals/execution are reset.
withdraw_token
Purpose: Withdraw USDC from escrow
Signers: authority, payer
Parameters:
nonce: u64amount: u64
Key Accounts:
authority(mut, signer) — token ownerpayer(mut, signer)backend(mut)user1(mut),user2(mut)trade(mut)source_escrow(mut)usdc_mintsource_escrow_usdc_ata(mut)user_usdc_ata(mut, init_if_needed)token_program,associated_token_program,system_program
Preconditions:
- Trade status is
Open,Approved, orCancelled amount > 0andamount <= available_balance
State: If status is not Cancelled, approvals/execution are reset.
Constants
| Constant | Value | Description |
|---|---|---|
| MAX_ASSET_SIZE | 65 bytes | Maximum asset size |
| USER_ESCROW_LEN | 9 bytes | Account size for UserEscrow struct |
| RENT_THRESHOLD_LAMPORTS | 5,000,000 | Rent 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.
| Field | Type | Description |
|---|---|---|
| user1 | Pubkey | First user in the trade |
| user2 | Pubkey | Second user in the trade |
| nonce | u64 | Trade nonce |
| creator | Pubkey | Account that created the trade |
| trade | Pubkey | Trade account address |
Error Reference
| Code | Name | Description |
|---|---|---|
| 6000 | UnauthorizedAccount | Signer does not match required authority |
| 6001 | TradeAlreadyApproved | User has already approved this trade |
| 6002 | InvalidEscrowAccount | Escrow account does not match expected PDA |
| 6003 | InvalidTokenAccount | Token account does not match expected ATA |
| 6004 | MintMismatch | NFT mint does not match instruction data |
| 6005 | NoAssetsTransferred | Empty asset list provided |
| 6006 | CannotWithdrawInCurrentStatus | Trade status does not allow withdrawals |
| 6007 | InvalidTradeStatus | Trade status does not support the operation |
| 6008 | TradeAlreadyExecuted | User has already executed this trade |
| 6009 | InsufficientFunds | Requested amount exceeds available balance |
| 6010 | InvalidRentDestination | Rent destination account is invalid |
| 6011 | InvalidMint | Mint address does not match program USDC mint |
| 6012 | TradingPaused | Trading is temporarily paused |
| 6013 | UserNotApproved | User has not approved before execution |
Common Failure Scenarios
| Scenario | Error | Resolution |
|---|---|---|
| Trading paused | TradingPaused | Retry when trading is resumed |
| Approving twice | TradeAlreadyApproved | Already approved; no action needed |
| Executing before approval | UserNotApproved | Call approve_trade first |
| Executing twice | TradeAlreadyExecuted | Already executed; no action needed |
| Withdrawing after execution | CannotWithdrawInCurrentStatus | Cannot withdraw after execution |
| Wrong trade status | InvalidTradeStatus | Confirm status and use valid instruction |
| Insufficient USDC withdrawal amount | InsufficientFunds | Reduce withdrawal amount |
| Wrong USDC mint | InvalidMint | Use 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
Approvedwithout both approvals - Trade cannot become
Executedwithout 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_tradereturns 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
| Tradeoff | Description |
|---|---|
| Escrow-based custody | Assets move into escrow during trade lifecycle. This improves deterministic settlement but adds explicit deposit/withdraw steps. |
| Nonce-based uniqueness | Concurrent trades between the same users are supported, but each trade requires unique nonce management. |
| Multi-step execution | Approval, execution, and settlement are separate transactions, increasing UX complexity in exchange for explicit mutual consent. |
| Rent threshold behavior | Low-balance users may have rent redirected to backend to reduce close-time failure risk. |
External Program Dependencies
| Program | ID | Usage |
|---|---|---|
| MPL Token Metadata | metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s | pNFT transfer and token record management |
| Metaplex Bubblegum | BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY | cNFT transfers |
| SPL Account Compression | mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW | Merkle tree verification |
| SPL Noop | mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3 | Log wrapper for Bubblegum |
| MPL Token Auth Rules | auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg | pNFT 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.