Overview
The sponsored_cctp_src_periphery program is a source chain periphery that enables users to initiate sponsored or non-sponsored cross-chain flows with custom Across-supported destination logic. It uses Circle’s CCTP v2 as the underlying bridge mechanism.
Program ID : CPr4bRvkVKcSCLyrQpkZrRrwGzQeVAXutFU8WupuBLXq
Purpose
This program enables:
Sponsored deposits - Quote-based deposits where fees are pre-calculated off-chain
CCTP bridging - Native USDC bridging via Circle’s protocol
Flexible routing - Support for direct-to-core, arbitrary actions, and swap flows
Gasless deposits - Users pay fees in bridged tokens, not SOL
Quote verification - EVM signature verification for sponsored quotes
Architecture
State Management
Program State
#[account]
pub struct State {
pub source_domain : u32 , // CCTP domain for Solana (5)
pub signer : Pubkey , // Trusted EVM signer for quote authorization
pub current_time : u32 , // Test mode only (0 on mainnet)
}
PDA : ["state"]
Minimum Deposit
#[account]
pub struct MinimumDeposit {
pub amount : u64 , // Minimum deposit amount for this token
}
PDA : ["minimum_deposit", burn_token.key()]
Used Nonce
#[account]
pub struct UsedNonce {
pub nonce : [ u8 ; 32 ], // Quote nonce (prevents replay)
pub deadline : u32 , // Quote expiry timestamp
}
PDA : ["used_nonce", nonce]
Rent Claim
#[account]
pub struct RentClaim {
pub amount : u64 , // Lamports owed to user from rent_fund
}
PDA : ["rent_claim", user.key()]
Rent Fund
A system account PDA used to sponsor account creation:
PDA : ["rent_fund"]
Bump : 255 (optimized)
Core Instructions
Initialization
pub fn initialize (
ctx : Context < Initialize >,
params : InitializeParams
) -> Result <()>
InitializeParams :
pub struct InitializeParams {
pub source_domain : u32 , // CCTP domain ID (5 for Solana)
pub signer : Pubkey , // EVM signer address (as Pubkey)
}
Initializes the program state. Only callable once by the upgrade authority.
Example :
anchor run initializeSponsoredCctpSrc \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR -- \
--quoteSigner 0xA1b2C3d4E5f6789012345678901234567890aBcD
Set Quote Signer
pub fn set_signer (
ctx : Context < SetSigner >,
params : SetSignerParams
) -> Result <()>
SetSignerParams :
pub struct SetSignerParams {
pub new_signer : Pubkey , // New EVM signer address
}
Updates the trusted quote signer. Only callable by upgrade authority.
Setting the signer to Pubkey::default() or an invalid address will effectively disable all deposits.
Set Minimum Deposit Amount
pub fn set_minimum_deposit_amount (
ctx : Context < SetMinimumDepositAmount >,
params : SetMinimumDepositAmountParams ,
) -> Result <()>
SetMinimumDepositAmountParams :
pub struct SetMinimumDepositAmountParams {
pub amount : u64 , // New minimum deposit amount
}
Configures the minimum deposit amount for a burn token. Must be set for each supported token.
Example :
# Set minimum deposit for USDC (0 for testing, higher for production)
anchor run setMinimumDepositSponsoredCctpSrc \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR -- \
--burnToken EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
--amount 1000000 # 1 USDC (6 decimals)
Deposit For Burn
pub fn deposit_for_burn (
ctx : Context < DepositForBurn >,
params : DepositForBurnParams
) -> Result <()>
DepositForBurnParams :
pub struct DepositForBurnParams {
pub quote : SponsoredCCTPQuote , // Quote details
pub signature : [ u8 ; 65 ], // EVM signature (r, s, v)
}
SponsoredCCTPQuote :
pub struct SponsoredCCTPQuote {
pub source_domain : u32 , // CCTP source domain (5)
pub destination_domain : u32 , // CCTP destination domain
pub mint_recipient : [ u8 ; 32 ], // Recipient on destination (32 bytes)
pub amount : u64 , // Amount to burn
pub burn_token : Pubkey , // Token to burn (e.g., USDC)
pub destination_caller : [ u8 ; 32 ], // Authorized caller on destination
pub max_fee : u64 , // Maximum fee in burn token units
pub min_finality_threshold : u32 , // Minimum finality before attestation
pub nonce : [ u8 ; 32 ], // Unique nonce (prevents replay)
pub deadline : u32 , // Quote expiry timestamp
pub max_bps_to_sponsor : u16 , // Max basis points to sponsor
pub max_user_slippage_bps : u16 , // User's slippage tolerance
pub final_recipient : [ u8 ; 32 ], // Final recipient address
pub final_token : [ u8 ; 32 ], // Final token after swaps
pub execution_mode : u8 , // 0=DirectToCore, 1=ArbitraryToCore, 2=ArbitraryToEVM
pub action_data : Vec < u8 >, // Encoded action data
}
Verifies a sponsored quote, records its nonce, and burns tokens via CCTP.
Execution Modes
DirectToCore (0) - Direct deposit to Across with no intermediate actions
ArbitraryActionsToCore (1) - Execute actions then deposit to Across
ArbitraryActionsToEVM (2) - Execute arbitrary EVM actions on destination
Example :
import { Program , AnchorProvider , BN } from "@coral-xyz/anchor" ;
import { SponsoredCctpSrcPeriphery } from "./target/types/sponsored_cctp_src_periphery" ;
import { Keypair , PublicKey } from "@solana/web3.js" ;
import { ethers } from "ethers" ;
const program = anchor . workspace . SponsoredCctpSrcPeriphery as Program < SponsoredCctpSrcPeriphery >;
// Step 1: Get quote from off-chain service
const quote = {
sourceDomain: 5 , // Solana
destinationDomain: 0 , // Ethereum
mintRecipient: ethers . utils . arrayify ( recipientAddress ),
amount: new BN ( 1_000_000 ), // 1 USDC
burnToken: usdcMint ,
destinationCaller: ethers . utils . arrayify ( "0x..." ),
maxFee: new BN ( 10_000 ), // 0.01 USDC fee
minFinalityThreshold: 1 ,
nonce: randomBytes32 (),
deadline: Math . floor ( Date . now () / 1000 ) + 3600 , // 1 hour
maxBpsToSponsor: 100 , // 1%
maxUserSlippageBps: 50 , // 0.5%
finalRecipient: ethers . utils . arrayify ( finalRecipient ),
finalToken: ethers . utils . arrayify ( finalTokenAddress ),
executionMode: 0 , // DirectToCore
actionData: [],
};
// Step 2: Get EVM signature from trusted signer
const signature = await getQuoteSignature ( quote ); // Off-chain signing
// Step 3: Execute deposit
const [ statePDA ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( "state" )],
program . programId
);
const [ rentFundPDA ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( "rent_fund" )],
program . programId
);
const [ usedNoncePDA ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( "used_nonce" ), quote . nonce ],
program . programId
);
const [ minimumDepositPDA ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( "minimum_deposit" ), usdcMint . toBuffer ()],
program . programId
);
const messageSentEventData = Keypair . generate ();
await program . methods
. depositForBurn ({ quote , signature })
. accounts ({
signer: user . publicKey ,
state: statePDA ,
rentFund: rentFundPDA ,
minimumDeposit: minimumDepositPDA ,
usedNonce: usedNoncePDA ,
depositorTokenAccount: userUsdcATA ,
burnToken: usdcMint ,
messageSentEventData: messageSentEventData . publicKey ,
// ... CCTP accounts
})
. signers ([ messageSentEventData ])
. rpc ();
Reclaim Event Account
pub fn reclaim_event_account (
ctx : Context < ReclaimEventAccount >,
params : ReclaimEventAccountParams
) -> Result <()>
ReclaimEventAccountParams :
pub struct ReclaimEventAccountParams {
pub attestation : Vec < u8 >, // CCTP attestation
pub nonce : [ u8 ; 32 ], // Message nonce
pub finality_threshold_executed : u32 , // Finality threshold
pub fee_executed : [ u8 ; 32 ], // Fee amount (uint256 BE)
pub expiration_block : [ u8 ; 32 ], // Expiration block (uint256 BE)
}
Reclaims rent from CCTP MessageSent event accounts after attestation. Returns lamports to rent_fund.
Timing :
Must wait for CCTP attestation service to process the message
Minimum time window defined by EVENT_ACCOUNT_WINDOW_SECONDS in CCTP program
Track closable accounts via CreatedEventAccount events
Reclaim Used Nonce Account
pub fn reclaim_used_nonce_account (
ctx : Context < ReclaimUsedNonceAccount >,
params : UsedNonceAccountParams ,
) -> Result <()>
UsedNonceAccountParams :
pub struct UsedNonceAccountParams {
pub nonce : [ u8 ; 32 ], // Nonce to close
}
Closes a used_nonce PDA after its quote deadline has passed, returning rent to rent_fund.
Helper Function :
pub fn get_used_nonce_close_info (
ctx : Context < GetUsedNonceCloseInfo >,
_params : UsedNonceAccountParams ,
) -> Result < UsedNonceCloseInfo >
Returns:
pub struct UsedNonceCloseInfo {
pub can_close_after : u32 , // Timestamp when closable
pub can_close_now : bool , // Whether closable now
}
Rent Fund Management
Withdraw Rent Fund
pub fn withdraw_rent_fund (
ctx : Context < WithdrawRentFund >,
params : WithdrawRentFundParams
) -> Result <()>
WithdrawRentFundParams :
pub struct WithdrawRentFundParams {
pub amount : u64 , // Lamports to withdraw
}
Withdraws lamports from rent_fund to arbitrary recipient. Only callable by upgrade authority.
Repay Rent Fund Debt
pub fn repay_rent_fund_debt (
ctx : Context < RepayRentFundDebt >
) -> Result <()>
Repays rent_fund liability accrued when rent_fund had insufficient balance during a deposit. Closes the rent_claim PDA and transfers owed lamports to the user.
Events
Emitted when a sponsored deposit is successfully executed:
#[event]
pub struct SponsoredDepositForBurn {
pub burn_token : Pubkey ,
pub amount : u64 ,
pub depositor : Pubkey ,
pub mint_recipient : [ u8 ; 32 ],
pub destination_domain : u32 ,
pub destination_caller : [ u8 ; 32 ],
pub max_fee : u64 ,
pub quote_nonce : [ u8 ; 32 ],
pub quote_deadline : u32 ,
pub execution_mode : u8 ,
// ... additional fields
}
CreatedEventAccount
Emitted with the address of the CCTP MessageSent event account:
#[event]
pub struct CreatedEventAccount {
pub event_account : Pubkey , // Address of MessageSent account
pub nonce : [ u8 ; 32 ], // Associated message nonce
}
Quote Verification
The program verifies EVM signatures using Solana’s secp256k1_recover syscall:
use solana_program :: secp256k1_recover :: secp256k1_recover;
// Compute EIP-191 message hash
let message_hash = keccak256 (
& [ b" \x19 Ethereum Signed Message: \n " , message_len , & serialized_quote ]
);
// Recover signer from signature
let recovered_pubkey = secp256k1_recover (
& message_hash ,
signature . recovery_id,
& signature . signature
) ? ;
// Verify against trusted signer
require! (
keccak256 ( & recovered_pubkey )[ 12 .. ] == state . signer . to_bytes ()[ .. 20 ],
ErrorCode :: InvalidSignature
);
Rent Management
The program requires rent for two types of accounts:
UsedNonce PDAs - ~100 bytes, closable after quote deadline
CCTP MessageSent accounts - ~1KB, closable after attestation
The rent_fund PDA sponsors these accounts:
Sufficient balance - Normal operation, rent deducted from rent_fund
Insufficient balance - Creates rent_claim PDA tracking debt to user
Reclaim - Operator closes accounts, returns rent to rent_fund
Repayment - User claims owed rent when rent_fund is replenished
Best Practices :
Monitor rent_fund balance
Track closable accounts via events
Regularly reclaim accounts to recover rent
Repay users if debt accumulates
Security Considerations
Quote Signature Verification
All sponsored deposits require valid EVM signatures:
Nonce tracking - Each nonce can only be used once
Deadline enforcement - Quotes expire after deadline
Signer trust - Only upgrade authority can change signer
Replay protection - Nonces are globally unique
Minimum Deposit Amounts
Prevents dust attacks and ensures economic viability:
require! (
params . quote . amount >= minimum_deposit . amount,
ErrorCode :: DepositTooSmall
);
Upgrade Authority
Critical functions restricted to upgrade authority:
initialize() - One-time setup
set_signer() - Change quote signer
set_minimum_deposit_amount() - Update minimums
withdraw_rent_fund() - Withdraw rent fund
Deployment
Build
unset IS_TEST
yarn build-svm-solana-verify
yarn generate-svm-artifacts
Deploy
export RPC_URL = https :// api . mainnet-beta . solana . com
export KEYPAIR =~ /. config / solana / deployer . json
export PROGRAM = sponsored_cctp_src_periphery
export PROGRAM_ID = $( cat target/idl/ $PROGRAM .json | jq -r ".address" )
export MULTISIG =< squads_vault_address >
export MAX_LEN = $(( 2 * $( stat -c %s target/deploy/ $PROGRAM .so ) ))
# Deploy
solana program deploy \
--url $RPC_URL \
--keypair $KEYPAIR \
--program-id target/deploy/ $PROGRAM -keypair.json \
--max-len $MAX_LEN \
--with-compute-unit-price 100000 \
--max-sign-attempts 100 \
--use-rpc \
target/deploy/ $PROGRAM .so
# Transfer authority
solana program set-upgrade-authority \
--url $RPC_URL \
--keypair $KEYPAIR \
--skip-new-upgrade-authority-signer-check \
$PROGRAM_ID \
--new-upgrade-authority $MULTISIG
# Upload IDL
anchor idl init \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR \
--filepath target/idl/ $PROGRAM .json \
$PROGRAM_ID
anchor idl set-authority \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR \
--program-id $PROGRAM_ID \
--new-authority $MULTISIG
Initialize
# Initialize state
anchor run initializeSponsoredCctpSrc \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR -- \
--quoteSigner 0xA1b2C3d4E5f6789012345678901234567890aBcD
# Set minimum deposit for USDC
export USDC_MINT = EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
anchor run setMinimumDepositSponsoredCctpSrc \
--provider.cluster $RPC_URL \
--provider.wallet $KEYPAIR -- \
--burnToken $USDC_MINT \
--amount 1000000 # 1 USDC minimum
# Fund rent_fund PDA
export RENT_FUND_PDA = $( solana address --program-id $PROGRAM_ID --seed rent_fund )
solana transfer \
--url $RPC_URL \
--keypair $KEYPAIR \
$RENT_FUND_PDA \
10 # 10 SOL for rent sponsorship
Integration Example
Off-Chain Quote Service
import { ethers } from "ethers" ;
import { Keypair } from "@solana/web3.js" ;
// EVM signer private key
const signer = new ethers . Wallet ( process . env . QUOTE_SIGNER_KEY ! );
async function generateQuote (
userRequest : DepositRequest
) : Promise <{ quote : SponsoredCCTPQuote ; signature : Buffer }> {
// Calculate fees and build quote
const quote = {
sourceDomain: 5 ,
destinationDomain: userRequest . destinationDomain ,
mintRecipient: ethers . utils . arrayify ( userRequest . recipient ),
amount: userRequest . amount ,
burnToken: userRequest . burnToken ,
destinationCaller: ethers . utils . arrayify ( userRequest . destinationCaller ),
maxFee: calculateMaxFee ( userRequest ),
minFinalityThreshold: 1 ,
nonce: crypto . randomBytes ( 32 ),
deadline: Math . floor ( Date . now () / 1000 ) + 3600 ,
maxBpsToSponsor: 100 ,
maxUserSlippageBps: userRequest . slippageBps ,
finalRecipient: ethers . utils . arrayify ( userRequest . finalRecipient ),
finalToken: ethers . utils . arrayify ( userRequest . finalToken ),
executionMode: userRequest . executionMode ,
actionData: userRequest . actionData ,
};
// Serialize quote (Anchor borsh format)
const serialized = serializeQuote ( quote );
// Sign with EIP-191
const messageHash = ethers . utils . hashMessage ( serialized );
const sig = await signer . signMessage ( serialized );
const { r , s , v } = ethers . utils . splitSignature ( sig );
// Encode as [r(32), s(32), v(1)]
const signature = Buffer . concat ([
Buffer . from ( r . slice ( 2 ), "hex" ),
Buffer . from ( s . slice ( 2 ), "hex" ),
Buffer . from ([ v ]),
]);
return { quote , signature };
}
Testing
# Run tests
yarn test-svm
# Test specific scenarios
anchor test --skip-build --skip-deploy -- --grep "sponsored deposit"
Maintenance
Monitoring
Track events for operational tasks:
// Monitor CreatedEventAccount events
program . addEventListener ( "CreatedEventAccount" , async ( event ) => {
const { eventAccount , nonce } = event ;
// Schedule reclaim after attestation + window
scheduleReclaim ( eventAccount , nonce );
});
// Monitor SponsoredDepositForBurn events
program . addEventListener ( "SponsoredDepositForBurn" , async ( event ) => {
const { quoteNonce , quoteDeadline } = event ;
// Schedule nonce reclaim after deadline
scheduleNonceReclaim ( quoteNonce , quoteDeadline );
});
Rent Reclamation
import { Program } from "@coral-xyz/anchor" ;
async function reclaimExpiredNonces () {
const nonces = await getExpiredNonces (); // From indexed events
for ( const nonce of nonces ) {
const [ usedNoncePDA ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( "used_nonce" ), nonce ],
program . programId
);
// Check if closable
const info = await program . methods
. getUsedNonceCloseInfo ({ nonce })
. accounts ({ state: statePDA , usedNonce: usedNoncePDA })
. view ();
if ( info . canCloseNow ) {
await program . methods
. reclaimUsedNonceAccount ({ nonce })
. accounts ({
state: statePDA ,
rentFund: rentFundPDA ,
usedNonce: usedNoncePDA ,
})
. rpc ();
}
}
}
Resources
Source Code View on GitHub
CCTP Documentation Circle CCTP docs
EIP-191 Signed data standard
Bug Bounty Report security issues