Summary

Type: DeFi
Timeline: From 2026-05-25 → To 2026-05-27
Languages: Solidity

Findings
Total issues: 1 (0 resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 0 (0 resolved) · Low: 1 (0 resolved)

Notes & Additional Information
0 notes raised (0 resolved)

Client Reported Issues
0 issues reported (0 resolved)

Table of Contents

Scope

OpenZeppelin conducted a diff audit of the across-protocol/contracts repository covering the changes between base snapshot b4c4a467 and target snapshot fe7182f3.

In scope were the following modified or added files:

 contracts
    ├── interfaces
    │   ├── ICounterfactualDeposit.sol
    │   ├── ICounterfactualDepositFactory.sol
    │   ├── ICounterfactualImplementation.sol
    │   └── IRoutePolicy.sol
    └── periphery
        └── counterfactual
            ├── AdminWithdrawManager.sol
            ├── CloneIdentity.sol
            ├── CounterfactualCloneArgs.sol
            ├── CounterfactualDeposit.sol
            ├── CounterfactualDepositCCTP.sol
            ├── CounterfactualDepositFactory.sol
            ├── CounterfactualDepositFactoryTron.sol
            ├── CounterfactualDepositOFT.sol
            ├── CounterfactualDepositSpokePool.sol
            ├── RoutePolicyImmutableRoot.sol
            ├── WithdrawImplementation.sol
            └── WithdrawImplementationTron.sol

The audit also covered PR #1446, a diff between base snapshot 8a45d6eb and target snapshot 36225955, introducing a Tron-specific MulticallHandler variant that drains leftover ERC20 balances through TronTransferLib. The scope consisted of the changes made to the following files:

 contracts
    ├── handlers
    │   ├── MulticallHandler.sol
    │   └── TronMulticallHandler.sol
    └── tron
        └── TronPeripheryImports.sol

System Overview

Across Protocol supports counterfactual deposits, where a user computes a deterministic address, sends tokens or native currency to it before any code is deployed there, and later has those funds acted upon. Each such address is a minimal proxy (EIP-1167) created through CounterfactualDepositFactory, and every clone delegatecalls a single shared dispatcher, CounterfactualDeposit. Because each clone is the context for the delegatecalls, token balances and signature domains are scoped to the clone address while the original caller and call value are preserved along the call chain.

The audited changes move route authorization out of each clone and into a dedicated policy contract. Previously, the Merkle root that authorizes a clone's permitted actions was stored directly in the clone's immutable arguments, fixing the route set at deployment time. The clone's immutable argument is now a hash of a CloneArgs identity tuple consisting of the output token, destination chain, recipient, user address, and a route policy address. At execution time, the dispatcher resolves the active Merkle root from the referenced IRoutePolicy contract rather than from the clone itself, so the authorized route set can change without redeploying clones. The in-scope policy implementation, RoutePolicyImmutableRoot, holds its root in the implementation bytecode behind a UUPS proxy and rotates the root through contract upgrades.

Dispatch and route authorization. On each execution, the dispatcher recomputes the CloneArgs hash and rejects any mismatch, then follows one of two paths. If the caller is the clone's user address, the Merkle check is skipped and any implementation may be invoked. Otherwise, the dispatcher verifies that a leaf committing the implementation address and the route parameters is included under the policy's active root before delegatecalling the implementation. Implementations that depend on clone identity, such as the bridge routes, additionally require the route parameters to match the clone's output token and destination chain through the CloneIdentity helper.

Bridge and withdrawal implementations. Three bridge implementations were in scope: CounterfactualDepositCCTP, CounterfactualDepositOFT, and CounterfactualDepositSpokePool. Each validates a local EIP-712 signature from a trusted signer that authorizes execution-time parameters, and the CCTP and OFT routes additionally forward a separate quote signature unchanged to their underlying periphery contracts, which were not in scope. WithdrawImplementation sweeps tokens or native currency from a clone and is reached either directly through the dispatcher or through AdminWithdrawManager, the privileged withdrawal entrypoint. Audit scope also contained the Tron-specific variants of the factory and the withdrawal implementation.

Actors. A depositor funds a clone address. A submitter, which can be any party willing to pay gas, lands the execution transaction on behalf of an authorized action and receives an execution fee. An off-chain signer authorizes execution-time parameters. A policy operator controls the Merkle root served by the route policy. The withdrawal entrypoint distinguishes a fully trusted direct withdrawer from a signature-gated path used for user-initiated withdrawals.

Security Model and Trust Assumptions

Signer. Each bridge implementation verifies a local EIP-712 signature from an immutable signer that authorizes execution-time parameters, and the scope of that authority differs by route. In the CounterfactualDepositSpokePool route, the signer controls all dynamic execution parameters, including the input and output amounts, the exclusive relayer and its exclusivity deadline, the quote timestamp, the fill deadline, and the execution fee. A signer that is malicious or compromised can therefore drain a clone through repeated quotes whose fees remain within the configured fixed maximum, temporarily lock funds in deposits that cannot be filled by setting an output amount above the input amount, or grief execution by naming an unreachable exclusive relayer. Signed executions on this route are not constrained by a per-clone nonce or a cooldown. In the CCTP and OFT routes, the signer only controls the execution fee, bounded by a per-route maximum, while the bridge quote itself is covered by a separate periphery signature, which narrows the signer's authority on those routes.

Submitter. Any party can submit an authorized execution and is compensated with an execution fee. The recipient of that fee is supplied at execution time and is not covered by the signer's EIP-712 signature in any of the three bridge implementations. As a result, an observer can copy a pending execution from the mempool, replace the fee recipient with an address it controls, and claim the fee, leaving the original submitter to pay gas without compensation. This is consistent with an open execution model in which the party that lands the transaction selects where its fee is paid.

Clone user. The user address recorded in a clone's identity is the canonical authority for that clone. Through the dispatcher's user-escape, this address can invoke any implementation directly without a Merkle proof and independent of the route policy, including when the policy's active root is unset. It is also the destination enforced for signature-gated withdrawals. Integrators are responsible for setting the user address to an account they control and not to a permissionless forwarding contract, which would expose the escape path to arbitrary callers.

Policy operator. The owner of the route policy proxy controls the active Merkle root and therefore the set of routes that clones bound to that policy may execute. For RoutePolicyImmutableRoot, updating the root is a UUPS upgrade to a new implementation carrying the new root. The policy operator is trusted to publish roots that authorize only intended routes, since an incorrect root can authorize unintended actions or leave clones unable to execute.

Withdrawal roles. AdminWithdrawManager exposes two withdrawal paths. The direct withdrawer is fully trusted and chooses the recipient of a withdrawal. The signature-gated path forces the recipient to the clone's user address, so a compromised signer on that path can cause a withdrawal to occur but cannot redirect the funds. WithdrawImplementation restricts its callers to its configured admin or the clone's user address.

Additional Considerations

The cross-chain message passed to the SpokePool deposit is part of the route parameters committed to the Merkle leaf, so it is fixed per leaf. A clone cannot vary the cross-chain execution payload per deposit, and each distinct message requires a separate leaf in the policy.

The CCTP, OFT, and SpokePool periphery contracts that the bridge implementations forward into are outside the audited scope, and their behavior is assumed to be correct. The same applies to the bridge quote signatures that the CCTP and OFT routes forward unchanged.

Low Severity

No Replay Protection in CounterfactualDepositSpokePool.execute

The CounterfactualDepositSpokePool contract's _verifySignature function hashes fields defined by EXECUTE_DEPOSIT_TYPEHASH, but the typehash contains no nonce and the implementation persists no "used digest" marker. Any observer can therefore replay the same calldata until signatureDeadline. Each successful replay performs another V3SpokePoolInterface(spokePool).deposit, which creates a fresh depositId in SpokePool.deposit, and pays executionFee again via executionFeeRecipient.call. This can cause duplicate deposits under identical parameters and repeated extraction of executionFee from any clone that is over-funded or later re-funded before expiry.

Consider adding single-use replay protection for SpokePool executions (for example, by including a per-clone nonce in EXECUTE_DEPOSIT_TYPEHASH and storing and consuming it, or by tracking a used mapping keyed by the EIP-712 digest). Consider consuming the nonce (or marking the digest) before any external calls to avoid partial-execution paths enabling replays.

Update: Acknowledged, not resolved.

Conclusion

Across Protocol's counterfactual deposit system allows users to receive funds at deterministic addresses and later have those funds bridged or deposited along pre-authorized routes. The audit scope included changes that move route authorization from a Merkle root fixed in each clone to an external route policy contract, together with the supporting clone-identity, signature, and withdrawal logic. Additionally, a narrow fix to correct the handling of USDT on Tron was also reviewed.

A low-severity issue was identified in the SpokePool execution path, where signed executions lack single-use replay protection and can be repeated until their deadline against a clone that holds or later receives sufficient funds. Several other behaviors that depend on correct off-chain operation, such as the unsigned execution-fee recipient and the concentration of parameter authority in the signer, are properties of the design rather than defects, and are surfaced as trust assumptions for the client's awareness.

The Across Protocol team is thanked for their cooperation and for providing context on the system throughout the engagement.

Appendix

Issue Classification

OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:

  • Critical
  • High
  • Medium
  • Low
  • Note/Information

Critical Severity

This classification is applied when the issue’s impact is catastrophic, threatening extensive damage to the client's reputation and/or causing severe financial loss to the client or users. The likelihood of exploitation can be high, warranting a swift response. Critical issues typically involve significant risks such as the permanent loss or locking of a large volume of users' sensitive assets or the failure of core system functionalities without viable mitigations. These issues demand immediate attention due to their potential to compromise system integrity or user trust significantly.

High Severity

These issues are characterized by the potential to substantially impact the client’s reputation and/or result in considerable financial losses. The likelihood of exploitation is significant, warranting a swift response. Such issues might include temporary loss or locking of a significant number of users' sensitive assets or disruptions to critical system functionalities, albeit with potential, yet limited, mitigations available. The emphasis is on the significant but not always catastrophic effects on system operation or asset security, necessitating prompt and effective remediation.

Medium Severity

Issues classified as being of medium severity can lead to a noticeable negative impact on the client's reputation and/or moderate financial losses. Such issues, if left unattended, have a moderate likelihood of being exploited or may cause unwanted side effects in the system. These issues are typically confined to a smaller subset of users' sensitive assets or might involve deviations from the specified system design that, while not directly financial in nature, compromise system integrity or user experience. The focus here is on issues that pose a real but contained risk, warranting timely attention to prevent escalation.

Low Severity

Low-severity issues are those that have a low impact on the client's operations and/or reputation. These issues may represent minor risks or inefficiencies to the client's specific business model. They are identified as areas for improvement that, while not urgent, could enhance the security and quality of the codebase if addressed.

Notes & Additional Information Severity

This category is reserved for issues that, despite having a minimal impact, are still important to resolve. Addressing these issues contributes to the overall security posture and code quality improvement but does not require immediate action. It reflects a commitment to maintaining high standards and continuous improvement, even in areas that do not pose immediate risks.