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: Configurable SPL Token
Overview
The swap protocol supports atomic, two-party exchanges across NFTs and tokens. Both parties deposit assets into program-controlled escrows, approve the trade, execute, and then the backend settles and closes the accounts.
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
- SPL Tokens: Any SPL token configured as the program's trade token
Key Features
- Escrow-based custody: Assets are held in PDA escrows during trade lifecycle
- Multi-asset support: NFTs, pNFTs, cNFTs, and SPL tokens 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
- Admin emergency controls: Config admin can force-cancel trades at any lifecycle stage
Assets are transferred into escrow once deposited. A trade only completes when both users approve, execute, and the backend marks it settled. 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, SPL tokens) 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
- Backend calls
mark_trade_settled→ status transitions toSettled - Trade and escrow PDAs are closed via
close_trade(backend only)
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(backend only)
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 | Both users executed; assets can be transferred via batch instructions |
| Settled | Backend marked trade settled; only close_trade or force_cancel_settled_trade allowed |
| Cancelled | Trade was cancelled; assets can be withdrawn, then close_trade closes PDAs |
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 (backend only)
Signers: backend
Parameters:
nonce: u64
Key Accounts:
backend(signer) — must beconfig.backend_accountuser1,user2creator(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
SettledorCancelled
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.
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 beconfig.backend_accountuser1,user2trade(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: 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 SPL tokens 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)spl_token_mint— must matchconfig.spl_token_mintsource_escrow_token_ata(mut)user1_token_ata(mut, init_if_needed)user2_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: 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 to Open.
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 to Open.
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 to Open.
withdraw_token
Purpose: Withdraw SPL tokens 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)spl_token_mint— must matchconfig.spl_token_mintsource_escrow_token_ata(mut)user_token_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 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 beconfig.config_adminconfiguser1,user2trade(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.
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 beconfig.config_adminconfiguser1,user2trade(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.
For exceptional post-settlement reversals — e.g., a settlement was confirmed on-chain but a dispute requires reversal before close_trade is called.
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 balance is below this, rent may route 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. 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, 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,
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 SPL token 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 token withdrawal amount | InsufficientFunds | Reduce withdrawal amount |
| Wrong SPL token mint | InvalidMint | Use 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
Approvedwithout both approvals - Trade cannot become
Executedwithout both execution flags - Either party can cancel before execution finalization
SPL Token Mint Validation
All token operations validate the mint address against the program's configured SPL token mint to prevent token substitution attacks.
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, settlement, and closure are separate transactions, increasing UX complexity in exchange for explicit mutual consent and backend-controlled settlement. |
| Rent threshold behavior | Low-balance users may have rent redirected to backend to reduce close-time failure risk. |
| Backend-only close | Only backend_account can call close_trade, ensuring controlled cleanup after settlement confirmation. |
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.