Summary
Type: Cross Chain
Timeline: From 2025-10-27 → To 2025-11-10
Languages: Rust
Findings
Total issues: 9 (8 resolved)
Critical: 0 (0 resolved) · High: 1 (1 resolved) · Medium: 1 (0 resolved) · Low: 3 (3 resolved)
Notes & Additional Information
3 notes raised (3 resolved)
OpenZeppelin audited the across-protocol/contracts repository at commit 30eed19.
In scope were the following files:
src
├── error.rs
├── event.rs
├── instructions
│ ├── admin.rs
│ ├── deposit.rs
│ └── mod.rs
├── lib.rs
├── state.rs
└── utils
├── mod.rs
├── signer.rs
├── sponsored_cctp_quote.rs
└── testable_utils.rs
The Sponsored CCTP Source Periphery is a Solana program that lets users initiate cross-chain transfers through Circle’s CCTP v2 while an operator sponsors rent for transient accounts (per-quote nonce PDA and CCTP’s MessageSent account). The periphery:
UsedNonce PDA per quoteThis program is intentionally minimal and acts as a UX layer that facilitates user-selected actions on the destination chain (see “Off-Chain Orchestration & Destination Execution”), while keeping all critical bridging invariants enforced by CCTP.
A “sponsored” deposit covers the lamports needed to create temporary accounts so that users do not have to.
rent_fund PDA (["rent_fund"]).deposit_for_burn):
UsedNonce PDA.UsedNonce at ["used_nonce", nonce] and records the quote’s deadline (the deadline is part of the signed quote - users do not set it).source_domain, and enforces deadline freshness.TokenMessengerMinterV2 deposit_for_burn_with_hook, designating rent_fund as the event rent payer for CCTP’s MessageSent account.message_sent_event_data address for later reclaim.reclaim_event_account CPIs into MessageTransmitterV2 to close message_sent_event_data, returning lamports to rent_fund.reclaim_used_nonce_account closes the per-quote UsedNonce PDA, returning its lamports to rent_fund.CCTP v2 is Circle’s burn-and-mint bridge. After the periphery validates the signed quote and records the nonce, it:
TokenMessengerMinterV2 and emits a Message stored in a MessageSent account (rent paid by rent_fund)MessageSent account rent via MessageTransmitterV2 once the event window has elapsedCCTP enforces denylist, fee bounds, token-pairing, message size, and finality constraints. The periphery pre-filters inputs, handles rent UX, and routes the call safely.
The periphery is a UX facilitator for actions executed on destination stacks:
MulticallHandlerFrom the UI, the user chooses the desired action. An off-chain component constructs the EVM-typed quote and encodes the appropriate hook data for CCTP (targets, calldata, and parameters for HyperCore/HyperEVM). The periphery verifies the quote/signature and passes the hook data through deposit_for_burn_with_hook, enabling seamless destination execution while preserving on-chain checks on the source side.
During the audit, the following trust assumptions were identified:
State. Security of this key is critical. Destination periphery/contracts are expected to re-validate the same quote.UsedNonce PDA. Closing is allowed only after the signed quote deadline.rent_fund. Otherwise, the deposit reverts before CCTP CPI.UsedNonce account.reclaim_event_account requires MessageTransmitterV2 to be unpaused. If paused, rent tied to MessageSent event accounts cannot be reclaimed, temporarily freezing those funds in rent_fund until unpaused.MessageSentSpace) and exact struct layout (MessageSent) of CCTP event accounts. It is assumed that CCTP upgrades will not modify the byte-layout or increase the initialization size of these accounts. Violations of this assumption could lead to Denial of Service during deposits (due to insufficient rent calculations) or permanently locked rent funds (due to deserialization failures during reclamation). The Across team must actively monitor Circle's CCTP changelogs and upgrade announcements to react immediately to such changes.Throughout the in-scope codebase, the following privileged roles were identified:
initialize: sets immutable source_domain and the initial trusted EVM signerset_signer: updates the trusted EVM signerwithdraw_rent_fund: can move lamports from rent_fund to any recipientState)
rent_fund and Triggers Repeated Temporary DoSThe deposit_for_burn function sponsors account rents so that users can initiate cross-domain burns with minimal friction. Each call refunds the user for the UsedNonce account rent while the periphery’s rent_fund pays to create the CCTPv2 MessageSent event account. The event’s space (and rent) scales with the message body length, which includes user-controlled hook data. Event-account rent is only reclaimable after a fixed five-day window, and reclaiming requires operational follow-through. The UsedNonce reclaim window appears short (≈1 hour from tests) but this value is not confirmed.
This creates a pronounced cost asymmetry. An attacker can obtain quotes and submit many deposits at the minimum token amount while maximizing hook data up to the protocol limit (e.g., near an 8192-byte message body), driving per-call MessageSent rent toward ~0.0579072 SOL. Since the periphery immediately refunds the user’s UsedNonce rent and pays the MessageSent rent from rent_fund, the attacker’s out-of-pocket cost is essentially just the network fee, whereas the periphery must lock significant lamports for at least five days per call.
The attacker can repeat this until rent_fund is depleted, then do it again whenever rent_fund is replenished or the reclaimed funds arrive. Since the MessageSent space is a direct function of the message body, arbitrary hook data (arbitrary actions on HyperEVM via MulticallHandler) lets the attacker deterministically maximize the sponsored rent on each attempt, further accelerating depletion. However, even without maximizing the message body size, the default sponsored costs remain high relative to the attacker’s near-zero cost, so the attack is still quite feasible.
The impact is a recurring, temporary DoS. Once rent_fund is drained, new deposits that rely on sponsorship cannot proceed, effectively halting normal user activity. The five-day reclaim window delays recovery, and operational requirements for reclaim add overhead. As the attacker can wait for top-ups or reclaimed lamports and then immediately drain them again, the system can be kept in a near-permanent DoS loop at very low attacker cost.
Consider shifting protections to the off-chain quote generator and using signature validation as the enforcement boundary.
1. Strengthen Off-Chain Controls (Primary Defense):
hook data length, well below the protocol maximum.2. Implement On-Chain Backstop:
minimum_deposit_amount check. This ensures basic economic symmetry even if off-chain controls fail or are misconfigured.3. Monitoring & Resilience:
rent_fund account to alert on depletion.rent_fund is empty, the transaction can still succeed by switching the event_rent_payer to the user and not refunding the UsedNonce rent. This allows the periphery to remain operational even when the rent_fund cannot cover the rent fees. If this fallback is implemented, consider updating reclaim_event_account and reclaim_used_nonce_account to return rent to the original payer rather than rent_fund.Update: Resolved in pull request #38, at commit 4f9cc6d.
The deposit_for_burn function invokes CCTP V2’s deposit_for_burn_with_hook function, which immediately burns tokens on the source chain and relies on destination-side message handling to run arbitrary hook logic. In this flow, the destination program must execute successfully for funds to be minted or released. If the destination hook reverts—for example due to time-sensitive checks or changing on-chain conditions—the receive path fails, and the message remains unprocessed, while the source-side burn remains irreversible.
CCTP V2 removes V1-style replacement/rescue entry points. As a result, once tokens are burned, parameters such as recipients or hook data cannot be changed. If the destination hook’s preconditions cannot be satisfied in future retries, the transfer can remain stuck indefinitely. Operational cleanup helpers like event-account reclamation do not recover value and do not mitigate this failure mode.
Consider pre-simulating the exact destination call (accounts, calldata/message body, and timing assumptions) before initiating the burn, and presenting a clear user-facing warning that no refund/unburn is available if the destination hook fails.
Update: Acknowledged, will resolve. The team has some partial mitigations in place and intends to add a UI-level notice to make end-users aware of this CCTP behavior. The team stated:
We acknowledge that under CCTP v2 the token burn on the source chain is irreversible - there is no “rescue” or “unburn” mechanism if the destination-side hook cannot complete. This is a fundamental constraint of CCTP v2, not something that can be addressed on the source chain. Our mitigation strategy therefore focuses on controlling destination behavior and off-chain pre-validation: ### 1. Strict Control of Mint Recipient & Hook Data * Users cannot choose arbitrary
mint_recipient; our quoting backend signs messages so that CCTP always mints to our single destination handlerSponsoredCCTPDstPeripherythat is currently also being audited. * Hook data is structured (SponsoredCCTPQuote) and signature-validated on destination; malformed or tampered messages should not revert - these simply should return early, keeping the funds safely in the periphery contract. We do recognize currently there is a potential issue ofSponsoredCCTPDstPeripherycontract reverting onabi.decodefailures when processing the received message, but this would be addressed as part of EVM contract audit process. This eliminates the risk of users directing mints to arbitrary contracts or untrusted hook receivers. ### 2. Destination Behavior Is Largely Non-Reverting AfterreceiveMessageOnce Circle’sreceiveMessagehas validated attestation and minted the bridged token to the periphery contract, the destination handler attempts to avoid reverts under normal operation: * Invalid quotes or expired signatures do not revert; they fall back to a “base-token only” path. * Market-dependent conditions (slippage too high, bridge liquidity unsafe, etc.) do not revert; execution switches to an EVM-fallback path. * For arbitrary-action flows, theMulticallHandleris configured with a non-zero fallback recipient (the periphery itself). * If any subcall reverts, the multicall wrapper suppresses the revert, emitsCallsFailed, and drains all tokens back to the periphery. * The periphery then treats this as a “no-swap/zero-output” scenario without reverting. Thus, many common time-sensitive hook failure scenarios (bad swap outcome, bad market price, arbitrary call reverting, etc.) would not cause destination-side reverts. ### 3. Off-Chain Pre-Simulation & UX Safeguard Before we ever perform adeposit_for_burn_with_hookon the source chain, the quoting service has full visibility into: * the exact encoded message, * the execution mode, * action data for arbitrary calls, * recipient, amounts, slippage settings, etc. We would be able to pre-simulate the destination call under current chain state. If simulation fails or conditions suggest the destination execution may not succeed reliably, the quote would not be issued and no burn occurs. We could also provide clear UI-level disclosure noting that CCTP v2 burns are final and cannot be reversed.
deposit_for_burnThe deposit_for_burn flow relies on the program’s rent_fund PDA to reimburse the user for the per-quote used_nonce account and fund the CCTP MessageSent event account via CPI. The instruction currently attempts the refund first (refund_used_nonce_creation) and later passes rent_fund as the event_rent_payer to the CCTP program. Both steps require sufficient lamports to be present in rent_fund at the time of submission.
However, there is no early validation ensuring that the rent_fund holds enough lamports to cover these obligations. If the rent_fund balance is insufficient, the transaction reverts when trying to transfer the refund amount to the user on refund_used_nonce_creation, wasting user fees and resulting in a poor user experience.
Consider adding an early balance check on rent_fund to fail fast before any CPI or account initialization work is performed and disabling UI actions when on-chain checks or cached telemetry indicate the rent_fund balance is below the threshold. In addition, consider supporting a non-sponsored fallback path that skips the refund and sets event_rent_payer to the user. If this path is enabled, consider updating reclaim_event_account and reclaim_used_nonce_account to return the rent to the original payer rather than rent_fund.
Update: Resolved in pull requests #57 and #76, at commit b9dd893.
The program derives the rent_fund and state PDAs without persisting their bump seeds, causing Anchor to search for a valid bump at runtime whenever these accounts are referenced. When a bump is omitted, Anchor uses find_program_address under the hood, which iteratively probes bump values and increases compute usage on every invocation. These PDAs are accessed in high-frequency, common functions and CPIs that form the program’s hot paths, in contrast to cold-path PDAs such as used_nonce, which are touched primarily during initialization (deposit_for_burn) and reclaimed once.
This design inflates and varies compute costs across common paths, which can push near-budget transactions over limits and create avoidable performance instability.
Consider persisting each PDA’s bump during initialization and referencing it explicitly in all constraints and invocations (e.g., #[account(seeds = [b"state"], bump = state.bump)]).
Update: Resolved in pull request #59, at commit b066738. The team stated:
They persisted the
statePDA bump seed in storage to avoid repeated bump searches, but did not do the same forrent_fundbecause it must remain system-owned (no data) to serve as the payer for CCTPMessageSentevent account creation, since the System Program can only debit lamports from accounts it owns. They considered pre-funding the event account by manually transferring lamports from a statefulrent_fund, but rejected this approach because it depends on accurate data size estimation and could break if CCTP's implementation changes. Instead, they keptrent_fundas a stateless, system-owned PDA that signs transfers using seeds, and optimized by selecting a program ID that results inrent_fundhaving bump = 255 (the highest possible value), minimizing the compute cost of bump searches while maintaining compatibility with CCTP's requirements.
emit_cpi! on Privileged Paths Increases Compute Cost Without Clear Security BenefitSeveral privileged/admin instructions (e.g., initialize, set_signer, and withdraw_rent_fund) emit events using emit_cpi!. CPI-based emission introduces an extra cross-program invocation for each event, adding serialization overhead and increasing compute unit consumption along administrative code paths that are already permissioned. The stated motivation of hardening against log truncation attacks is not compelling for these flows because the threat model inherently assumes cooperative behavior from upgrade authority and administrators.
The additional CPI can meaningfully raise the CU footprint of low-frequency but high-importance maintenance operations, raising the probability of CU-limit failures in composite transactions and lengthening execution time without providing material integrity guarantees. For similar reasons, CPI-backed events on non-critical, low-risk public flows such as reclaim_event_account or reclaim_used_nonce_account appear unjustified, since these events are unlikely to serve as authorization-critical signals and offer limited value against log truncation by an already trusted actor.
Consider replacing emit_cpi! with standard emit! for all privileged/admin instructions and non-critical public flows, retaining emit_cpi! only for public, user-triggered paths whose events are security-relevant (for example, deposit/mint/burn finalization signals that are consumed by off-chain components).
Update: Resolved in pull request #63, at commit 54c3fd7.
withdraw_rent_fundThe withdraw_rent_fund instruction does not validate that the recipient address is not Pubkey::default(). The function only checks that the withdrawal amount is positive and allows the upgrade authority to withdraw funds to any account, including the default pubkey. If the upgrade authority accidentally or maliciously specifies Pubkey::default() as the recipient, funds will be sent to an uncontrolled address. While the transaction will succeed, these lamports will effectively become irrecoverable, resulting in a permanent loss of funds from the rent_fund.
Consider adding a validation check to prevent withdrawals to the default pubkey.
Update: Resolved in pull request #60, at commit 28ae33d.
deposit_for_burn ParameterThis comment for the deposit_for_burn instruction in the lib.rs incorrectly states the quote parameter is ABI-encoded and fixed length. In fact, quote is a SponsoredCCTPQuote struct (variable length due to action_data), serialized by Anchor, not ABI-encoded. ABI encoding occurs inside the deposit_for_burn instruction when encode_hook_data() is called, not before. Misleading documentation can cause developers to misunderstand the input format and when ABI encoding occurs, leading to incorrect assumptions about the instruction's behavior.
Consider updating the comment to state that quote is a SponsoredCCTPQuote struct serialized by Anchor.
Update: Resolved in pull request #61, at commit 483e813.
current_time Field in Production State Struct Increases Storage and Compute CostsThe State struct includes a current_time: u64 field that is only used in test mode. In production, the field is initialized to 0 and never updated, while get_current_time() uses Clock::get() directly instead of reading from state.current_time. Despite being unused, the field still consumes 8 bytes of storage and requires additional compute units per state deserialization. This affects hot-path instructions like deposit_for_burn, and reclaim_used_nonce_account, which deserialize the state account on every invocation, causing unnecessary compute overhead.
Consider using a separate test-only account structure if compute becomes critical. Alternatively, consider documenting that current_time is unused in production and incurs additional compute cost during state deserialization in the hot path, in exchange for the testing benefit.
Update: Resolved in pull request #62, at commit d2413d7.
In sponsored-cctp-src-periphery, deposit_for_burn allows CCTP messages large enough that the corresponding reclaim_event_account call can no longer fit within Solana’s packet size limit.
deposit_for_burn funds a MessageSent event account and stores the source message; this transaction only needs a single user signature and fits under the limit.reclaim_event_account requires both destination_message and the CCTP attestation, where the latter contains multiple signatures and can push the transaction size beyond the 1232-byte limit.For messages near this boundary, deposit_for_burn succeeds but reclaim_event_account will always fail, making the event account impossible to close. This permanently locks the rent paid by the protocol and enables a griefing vector: an attacker can repeatedly submit “too-large-to-reclaim” messages to drain the rent fund and eventually disrupt new deposits until the fund is manually replenished. User-bridged funds remain safe, but protocol funds are lost and liveness is impacted.
To address this, the protocol should:
deposit_for_burn, derived from MAX_TX_SIZE minus a conservative upper bound for attestation and instruction overhead, rejecting messages that cannot be safely reclaimed.destination_message on-chain from the stored source message plus a few compact instruction arguments (e.g., nonce, finality threshold, fee, expiration), ensuring the reconstructed message exactly matches CCTP’s serialization and still passes hash_source_fields / attestation verification.Update: Resolved in pull request #75, at commit 6202968.
The Sponsored CCTP Source Periphery system facilitates cross-chain token transfers via Circle's CCTP v2 while sponsoring rent for transient accounts, thereby improving UX. The design distributes responsibilities among the operator, users, and the CCTP infrastructure, with the periphery handling quote verification, nonce uniqueness, and rent sponsorship while delegating critical bridging invariants to official CCTP v2 programs.
The most notable risk identified during the audit involves a DoS vulnerability whereby attackers can cheaply drain the rent_fund by repeatedly submitting deposits with minimal token amounts while maximizing hook data length, creating a severe cost asymmetry that allows repeated temporary DoS of the system. In addition, a medium-severity issue was identified whereby destination hook failures can permanently lock funds, as tokens are immediately burned on the source chain and CCTP v2 lacks recovery mechanisms for failed destination hooks. Multiple lower-severity issues were also observed, primarily related to insufficient early failure checks when the rent fund lacks sufficient lamports to sponsor account creation, missing validation checks for default recipient address in rent fund withdrawals, and inefficient compute unit usage.
The Risk Labs team is appreciated for their support and responsiveness during the audit.