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
- 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
Config
PDA Seeds: ["config"] (singleton)
| Field | Type | Description |
|---|---|---|
| config_admin | Pubkey | Admin authority for config changes and emergency actions |
| backend_account | Pubkey | Backend co-signer authorized to settle and close trades |
| spl_token_mint | Pubkey | The SPL token accepted in trades |
| trading_paused | bool | Global pause flag; blocks new trades and executions when set |
| config_bump | [u8; 1] | PDA bump seed |
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 |
| Cancelled | Trade was cancelled; assets can be withdrawn, then close_trade closes PDAs |
| Settled | Backend 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
| Field | Type | Description |
|---|---|---|
| bump | u8 | PDA 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.
| Instruction | Signer | Purpose |
|---|---|---|
initialize(config_admin, backend_account, spl_token_mint) | Program upgrade authority | Create the Config PDA and set the admin, backend co-signer, and trade token. Callable once. |
set_config_admin(config_admin) | config_admin | Rotate the config admin |
set_backend_account(backend_account) | config_admin | Rotate the backend co-signer |
set_trading_paused(paused) | config_admin | Pause 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 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_core_batch
Purpose: Transfer Metaplex Core assets from escrow to recipient
Signers: authority, payer
Parameters:
nonce: u64core_data: Vec<CoreTransferData>
Key Accounts:
authority(mut, signer) — user1, user2, or backendpayer(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 whenhas_collectionis 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 backendpayer(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 ATAssource_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: 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, mint_account]—mint_accountmust equal the trade'snft_mintor the call failsMintMismatch(6004)
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— the SPL mint being moved; the escrow and user accounts must be this mint's canonical ATAssource_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, orCancelled amount > 0andamount <= 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: u64core_data: CoreTransferData
Key Accounts:
authority(mut, signer) — asset ownerpayer(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, orCancelled
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 serialized asset entry size |
| USER_ESCROW_LEN | 9 bytes | Account size for UserEscrow struct |
| CONFIG_LEN | 106 bytes | Account 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.
| 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 |
| 6014 | InvalidAsset | Asset account is invalid — wrong owner or not a Metaplex Core asset |
Common Failure Scenarios
| Scenario | Error | Resolution |
|---|---|---|
| Trading paused | TradingPaused | Retry when trading is resumed |
| Approving twice | TradeAlreadyApproved | Already approved; no action needed |
| Executing before both approvals | InvalidTradeStatus | Trade status must be Approved — both parties must approve_trade first. (UserNotApproved is declared but never returned by the current program.) |
| 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
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_traderefunds 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
| 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. |
| 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 |
| MPL Core | CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d | Metaplex Core asset 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.