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: Configurable SPL Token
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 |
| 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
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 and the execute-batch instructions revert with TradingPaused |
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— 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.
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, 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 |
| 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 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_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.