News | OpenZeppelin

Deposit Flow Audit

Written by OpenZeppelin Security | April 2, 2026

Summary

Type: Cross Chain
Timeline: From 2026-02-23 → To 2026-03-02
Languages: Solidity

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

Notes & Additional Information
8 notes raised (6 resolved)

Scope

OpenZeppelin performed a diff audit of the across-protocol/contracts repository at commit 47c1bea9.

In scope were the changes made to the following files:

 contracts
  ├── TransferProxy.sol
  ├── chain-adapters
  │   └── DonationBox.sol
  ├── handlers
  │   └── HyperliquidDepositHandler.sol
  ├── interfaces
  │   ├── ICounterfactualDeposit.sol
  │   ├── ICounterfactualDepositFactory.sol
  │   └── ICounterfactualImplementation.sol
  ├── libraries
  │   └── TronClones.sol
  └── periphery
      ├── counterfactual
      │   ├── AdminWithdrawManager.sol
      │   ├── CounterfactualConstants.sol
      │   ├── CounterfactualDeposit.sol
      │   ├── CounterfactualDepositCCTP.sol
      │   ├── CounterfactualDepositFactory.sol
      │   ├── CounterfactualDepositFactoryTron.sol
      │   ├── CounterfactualDepositOFT.sol
      │   ├── CounterfactualDepositSpokePool.sol
      │   └── WithdrawImplementation.sol
      └── mintburn
          └── HyperCoreFlowExecutor.sol

System Overview

Across Protocol is a cross-chain bridge that uses an optimistic relay model: relayers fill user deposit intents on destination chains and are later reimbursed from a central HubPool on Ethereum mainnet. The protocol's smart contract suite includes SpokePool contracts deployed on each supported chain, a HubPool on L1, chain-specific adapters for L1-L2 messaging, and a growing periphery layer that handles token approvals, gasless deposits, and integration with third-party bridging standards such as CCTP (Circle) and OFT (LayerZero).

This audit covers six commits made to the master branch (dc576507..47c1bea9). The changes introduce two major features and one set of minor dependency upgrades:

  • Counterfactual Deposit System (primary scope): A new subsystem that lets users receive a deterministic, reusable deposit address before any contract is deployed. Users send tokens to a CREATE2-predicted address; a relayer later deploys a minimal EIP-1167 clone there and triggers the deposit in a single transaction. The architecture is Merkle-dispatched: each clone stores only a 32-byte Merkle root as its immutable argument, and every execution must supply a valid Merkle proof linking the requested (implementation, params) pair to that root. This enables a single address to support multiple bridge types (Across SpokePool, CCTP, OFT) plus a withdrawal escape hatch, all without redeploying.
  • TransferProxy: A stateless contract that mimics the SpokePool deposit interface but executes a same-chain ERC-20 transfer instead of a cross-chain bridge. This allows the existing SpokePoolPeriphery swap-and-bridge methods (permit, permit2, EIP-3009 gasless flows) to be reused for local-only swaps without any changes to the periphery contracts.
  • Minor changes: The DonationBox contract migrates from Ownable to AccessControl with a dedicated WITHDRAWER_ROLE, allowing multiple authorized addresses (e.g., both HyperliquidDepositHandler and HyperCoreFlowExecutor) to withdraw funds. The HyperliquidDepositHandler constructor is updated to accept an externally-deployed DonationBox address rather than deploying one internally. Several contracts (HyperliquidDepositHandler, HyperCoreFlowExecutor, DonationBox) have their OpenZeppelin imports migrated from contracts-v4 to contracts (v5).

Counterfactual Deposit Architecture

The counterfactual deposit system is composed of four architectural layers:

  • Factory layer: CounterfactualDepositFactory is a permissionless, stateless factory that deploys clones via Clones.cloneDeterministicWithImmutableArgs and forwards execution calldata. CounterfactualDepositFactoryTron extends the base factory to override address prediction with Tron's 0x41 CREATE2 prefix (via the TronClones library) instead of the EVM's 0xff.
  • Dispatcher layer: CounterfactualDeposit is the Merkle-dispatched proxy. All clones are instances of this contract. On each execute() call, it decodes the Merkle root from its immutable args, verifies the supplied proof against a leaf of keccak256(abi.encode(implementation, keccak256(params))), and delegatecalls into the proven implementation.
  • Implementation layer: Bridge-specific contracts that execute the actual deposit:
    • CounterfactualDepositSpokePool verifies an EIP-712 signature from a trusted signer, enforces a two-component fee cap (maxFeeFixed + maxFeeBps × amount), and calls SpokePool.deposit(). Uses no explicit nonce; replay protection comes from token-balance consumption and signature deadlines.
    • CounterfactualDepositCCTP and CounterfactualDepositOFT marshal parameters and delegate signature verification and nonce management to their respective SponsoredSrcPeriphery contracts.
    • WithdrawImplementation allows either an admin or the user to withdraw arbitrary tokens or native ETH from a clone address.
  • Admin layer: AdminWithdrawManager provides two trust-minimized withdrawal paths: a direct path for a privileged bot (directWithdrawer) and a signed path where anyone can trigger a withdrawal, but the recipient is hardcoded to the user address committed in the Merkle leaf.

Counterfactual Deposit Lifecycle

The end-to-end flow proceeds in three phases:

  1. Address prediction (off-chain). A Merkle tree is built whose leaves are keccak256(abi.encode(implementation, keccak256(params))) for each authorized action (deposit via SpokePool, deposit via CCTP, withdraw, etc.). The factory's predictDepositAddress(dispatcher, merkleRoot, salt) returns the deterministic clone address. This address is shared with the user.
  2. Funding. The user transfers tokens (or native ETH) to the predicted address. No contract exists there yet.
  3. Execution. A relayer calls factory.deployIfNeededAndExecute(), which deploys the clone if necessary and forwards the execute calldata. The clone's dispatcher verifies the Merkle proof, then delegatecalls into the target implementation (e.g., CounterfactualDepositSpokePool), which verifies signatures, validates fees, and calls into the underlying bridge protocol. An execution fee is paid to the relayer to incentivize timely execution.

TransferProxy Design

TransferProxy exposes deposit() and unsafeDeposit() with signatures matching SpokePool, but enforces three invariants: destinationChainId == block.chainid, inputToken == outputToken, and inputAmount == outputAmount. It then performs a simple safeTransferFrom from the caller to the recipient. If a non-empty message is provided and the recipient is a contract, it calls handleV3AcrossMessage() on the recipient, enabling composition with MulticallHandler.

Security Model and Trust Assumptions

The counterfactual deposit system's security relies on a combination of Merkle-proof-based authorization, EIP-712 cryptographic signatures, and trust in several privileged off-chain components. The TransferProxy is stateless and trustless by design. The integrity of the counterfactual system is contingent on correct off-chain Merkle tree construction, honest behavior of trusted signers, and proper configuration of all contracts.

  • Merkle Tree Construction: The Merkle root committed to each clone defines the complete set of authorized actions. A maliciously constructed tree could authorize arbitrary implementations or parameters. Tree construction occurs entirely off-chain by the Across API and is assumed to be correct. Specifically, the Merkle tree construction process is trusted to:
    • only include legitimate, audited implementation addresses as leaves
    • commit correct deposit parameters (fee bounds, token addresses, recipients) that reflect the user's intent
    • include exactly one WithdrawImplementation leaf with the correct admin and user addresses so that the escape hatch is always available
    • use unique salts to prevent address collisions between unrelated users
  • Bridge Signers: Each bridge implementation relies on a trusted signer to authorize execution parameters at the time of deposit. Signer compromise is bounded but not harmless:
    • SpokePool signer (immutable in CounterfactualDepositSpokePool): Signs EIP-712 structs controlling inputAmount, outputAmount, fillDeadline, and other execution parameters. A compromised signer could set unfavorable outputAmount values causing deposits to be unfillable, but the fee check (totalFee <= maxFeeFixed + maxFeeBps × inputAmount / BPS_SCALAR) bounds the maximum value extractable beyond what the user committed to at address-generation time. The signer is also trusted to set reasonable deadlines and not sign multiple conflicting quotes for the same clone balance.
    • CCTP and OFT signers (embedded in their respective SrcPeriphery contracts): Signature verification and nonce management are fully delegated to the SponsoredCCTPSrcPeriphery and SponsoredOFTSrcPeriphery contracts. These signers are similarly bounded by maxFeeBps parameters committed in the Merkle leaf.
  • Replay Protection Model: The CounterfactualDepositSpokePool implementation uses no explicit nonce. Replay protection relies on three complementary mechanisms:
    • Token-balance consumption: Executing a deposit drains the clone's token balance, preventing immediate replay.
    • Signature deadlines: Each signature includes a signatureDeadline after which it becomes invalid.
    • Clone-specific EIP-712 domain: The domain separator includes address(this) (the clone address), preventing cross-clone replay.

If a clone is re-funded after execution, a new signature with a new deadline is required from the signer. The CCTP and OFT implementations delegate nonce tracking to their respective SrcPeriphery contracts.

  • Fee Bound Assumptions: The CounterfactualDepositSpokePool uses a two-component fee cap where the stableExchangeRate (committed at address-generation time) converts between outputToken and inputToken to compute the relayer fee. This rate is assumed to remain accurate for the lifetime of the clone. For volatile or de-pegging token pairs, a stale exchange rate could cause the fee check to either over-constrain (blocking valid deposits) or under-constrain (allowing excessive fee extraction).
  • Withdrawal Escape Hatch: Each clone's Merkle tree includes a WithdrawImplementation leaf with both an admin address (typically AdminWithdrawManager) and a user address (the depositor's EOA or multisig). Either party can initiate a withdrawal independently, ensuring users retain access to their funds even if the admin infrastructure becomes unavailable. The AdminWithdrawManager's signed withdrawal path hardcodes the recipient to the user address from the Merkle leaf, preventing redirect attacks even if the signer is compromised.
  • TransferProxy Trust Model: TransferProxy is stateless, holds no funds, and requires explicit safeTransferFrom approval from the caller. The chain-ID, token-equality, and amount-equality checks prevent misuse as a cross-chain or token-swap primitive. It trusts that recipient contracts implementing handleV3AcrossMessage() will not perform harmful reentrant actions.
  • DonationBox Access Model: The DonationBox contract has been migrated from single-owner Ownable to role-based AccessControl. Any account granted WITHDRAWER_ROLE can withdraw arbitrary amounts of any token at any time, with no per-transaction limits. The security of sponsorship funds depends entirely on careful management of role grants by the DEFAULT_ADMIN_ROLE holder.
  • OpenZeppelin v4 to v5 Migration: Several contracts now import from OpenZeppelin v5. Notably, Ownable in v5 requires passing the initial owner to the constructor rather than defaulting to msg.sender, and ReentrancyGuard has been moved from security/ to utils/. Incorrect migration could lead to ownership or reentrancy guard misconfiguration.
  • Tron Compatibility: The CounterfactualDepositFactoryTron and TronClones library adapt address prediction for Tron's TVM, which uses 0x41 instead of 0xff as the CREATE2 derivation prefix. The actual deployment via Clones.cloneDeterministicWithImmutableArgs works unchanged on Tron (the create2 opcode natively uses the correct prefix), so only the prediction function is overridden. An incorrect prefix would cause predicted addresses to diverge from actual deployed addresses, resulting in funds sent to undeployable locations.

Privileged Roles

The counterfactual deposit system introduces several privileged roles across its contracts. The system is designed to minimize trust requirements — most contracts are permissionless and stateless, but the withdrawal management and signer infrastructure carry significant privileges. The security of the protocol relies on these roles being managed securely and operated honestly.

  • owner (AdminWithdrawManager, inherited from Ownable): The most powerful role in the withdrawal subsystem. It has the authority to:
    • update the directWithdrawer address via setDirectWithdrawer(), which controls who can perform unrestricted withdrawals from any clone
    • update the signer address via setSigner(), which controls who can authorize signed withdrawals to users
  • directWithdrawer (AdminWithdrawManager): A trusted address (intended for an automated bot or multisig) that can call directWithdraw() to withdraw funds from any clone to an arbitrary recipient. This is the most powerful withdrawal capability — unlike the signed path, it is not restricted to paying the committed user address.
  • signer (AdminWithdrawManager): Signs EIP-712 withdrawal authorizations that anyone can submit on-chain. The signed path is trust-minimized: the recipient is always forced to the user address committed in the clone's Merkle leaf, preventing redirect attacks even if this key is compromised. A compromised signer could trigger premature withdrawals but cannot steal funds.
  • signer (CounterfactualDepositSpokePool, immutable): Signs EIP-712 execution parameters (inputAmount, outputAmount, fillDeadline, etc.) for SpokePool deposits. This address is set at implementation deployment time and cannot be changed. The signer is trusted to provide fair quotes and not sign conflicting parameters for the same clone balance.
  • DEFAULT_ADMIN_ROLE (DonationBox): It can grant and revoke the WITHDRAWER_ROLE to any address, controlling which contracts or EOAs can withdraw sponsorship funds. This role is granted to the deployer at construction time.
  • WITHDRAWER_ROLE (DonationBox): It can withdraw any amount of any ERC-20 token held by the DonationBox. Intended to be granted to HyperliquidDepositHandler, HyperCoreFlowExecutor, and similar contracts that need to access sponsorship funds for account activation and bridging fees.
  • owner (HyperliquidDepositHandler, inherited from Ownable): It can update the signer and spokePool addresses, add supported tokens with their Hypercore bridge parameters, and sweep funds from the contract, the DonationBox, or Hypercore accounts. 

Medium Severity

EIP-712 Signature Not Bound to Merkle Leaf Enables Cross-Route Replay in CounterfactualDepositSpokePool

The EXECUTE_DEPOSIT_TYPEHASH used by CounterfactualDepositSpokePool for EIP-712 signature verification covers only execution-time parameters from SpokePoolSubmitterData: inputAmount, outputAmount, exclusiveRelayer, exclusivityDeadline, quoteTimestamp, fillDeadline, and signatureDeadline. It does not include any of the nine route-specific fields committed in SpokePoolDepositParams: destinationChainId, inputToken, outputToken, recipient, message, stableExchangeRate, maxFeeFixed, maxFeeBps, and executionFee. The EIP-712 domain separator binds the signature to the clone address but not to a specific leaf within that clone's Merkle tree.

The architecture is designed for a single clone to hold multiple deposit leaves targeting different destination chains — a single-destination clone has limited practical value. Each leaf commits to its own route configuration, including fee parameters calibrated to that destination's gas costs and relayer economics. However, because the signature is leaf-agnostic, an untrusted relayer who observes a pending signature intended for Leaf A can call clone.execute(implementation, paramsB, submitterData, proofB) using Leaf B's parameters and proof instead. The Merkle proof for Leaf B is independently valid, and the signature verification succeeds because none of the differing leaf fields are part of the signed struct. This grants the relayer unilateral choice over which route the user's funds take, enabling several attack vectors:

  • The relayer can extract the maximum executionFee across all leaves. The executionFee is deducted from inputAmount and transferred to executionFeeRecipient, which is supplied by the relayer in submitterData and not signed over. Because different destination chains have different gas costs, their leaves will carry different executionFee values. A rational relayer always picks the leaf with the highest one, pocketing max(executionFee) - min(executionFee) at the user's expense on every execution. This is a direct, guaranteed extraction with zero risk to the relayer.
  • The relayer can shop for the most permissive fee cap. The _checkFee function enforces totalFee <= maxFeeFixed + (maxFeeBps * inputAmount) / 10000, where each leaf has its own maxFeeFixed, maxFeeBps, and stableExchangeRate calibrated for its destination. A leaf targeting an expensive chain such as Ethereum mainnet will have a more generous cap than one targeting a cheap rollup. The relayer selects the leaf with the widest fee tolerance. A higher stableExchangeRate on that leaf further reduces the computed relayerFee inside _checkFee, compounding the leniency.
  • The user's deposit is bridged to a chain they did not request. The funds arrive at the recipient address committed in the chosen leaf, which may differ from what the user intended if different leaves specify different recipients — plausible when the user has distinct receiving addresses per chain or uses smart contract wallets that may not be deployed at the same address on all chains. In the worst case, the user does not control the recipient address on the relayer-chosen chain.
  • The message field passed through to SpokePool.deposit() can trigger arbitrary logic on the destination fill. If one leaf has an empty message while another encodes a complex swap or hook, the relayer can trigger actions the user did not authorize or cause a fill to revert on the wrong chain, forcing a slow refund process.

As a concrete scenario, consider a clone whose tree contains two SpokePool leaves: Leaf 0 targeting Arbitrum with executionFee = 0.5 USDC, maxFeeBps = 100 (1%), and maxFeeFixed = 0.5 USDC; and Leaf 1 targeting Ethereum mainnet with executionFee = 5 USDC, maxFeeBps = 500 (5%), and maxFeeFixed = 10 USDC. The signer authorizes inputAmount = 1000 and outputAmount = 950 intending Leaf 0. The relayer executes with Leaf 1: 5 USDC is taken as executionFee instead of 0.5 (4.5 USDC stolen directly), the _checkFee cap becomes 60 USDC instead of 10.5 USDC, and funds arrive on the mainnet instead of Arbitrum.

Consider including a commitment to the leaf identity in the signed struct, for example, by adding a paramsHash field (the keccak256 of the encoded SpokePoolDepositParams) to both the EXECUTE_DEPOSIT_TYPEHASH and the hash constructed in _verifySignature. This binds each signature to exactly one Merkle leaf, preventing cross-route replay while preserving the existing separation between committed route params and dynamic submitter data.

Update: Acknowledged, not resolved. However, the team added documentation for this limitation in pull request #1357. The team stated:

We decided that the counterfactual system will not be used in a way where there are multiple implementation type leafs on a given CounterfactualDeposit proxy clone. A need for multiple implementation types indicates a different route altogether, so a new CounterfactualDeposit proxy clone would be deployed for this route.

Low Severity

Setting the SpokePool depositor to the clone disables speed-ups and updated fills

Across V3 supports depositor-driven updates to an unfilled deposit (output amount, recipient, and message) via speedUpDeposit, and relayer-side execution of those updates via fillRelayWithUpdatedDeposit. Both paths verify a depositor signature using _verifyUpdateV3DepositMessage, which in turn calls _verifyDepositorSignature and ultimately SignatureChecker.isValidSignatureNow.

However, CounterfactualDepositSpokePool.execute hard-codes the depositor as the clone address when calling SpokePool.deposit. The clone is an EIP-1167 proxy whose dispatcher implementation (CounterfactualDeposit) does not implement EIP-1271 and exposes only receive() and execute() (CounterfactualDeposit). As a result, depositor signature checks for updates fail on the origin chain, and updated fills also fail on the destination chain because the destination SpokePool verifies the signature against the same relayData.depositor (fillRelayWithUpdatedDeposit). This makes counterfactual deposits effectively immutable after execution, increasing the likelihood of unfilled deposits and capital lock until fillDeadline and refund.

Consider setting depositor to an EOA that can sign updates (for example, a user-controlled refund recipient committed in the Merkle leaf), so speedUpDeposit and fillRelayWithUpdatedDeposit remain usable. Alternatively, consider deploying a deterministic EIP-1271 validator at the same depositor address on all supported destination chains and wiring counterfactual deposits to use that validator as depositor.

Update: Acknowledged, not resolved. However, the team added documentation for this limitation in pull request #1357. The team stated:

We decided that the additional complexity and risks that come with supporting speed-ups and updated fills through the SpokePool flow outweigh the upsides. Speed-ups and updated flows are very rarely (if at all) used currently. Supporting them is not a requirement for the counterfactual system.

Notes & Additional Information

Missing Zero-Address Checks

When operations with address parameters are performed, it is crucial to ensure the address is not set to zero. Setting an address to zero is problematic because it has special burn/renounce semantics. This action should be handled by a separate function to prevent accidental loss of access during value or ownership transfers.

Throughout the codebase, multiple instances of missing zero-address checks were identified:

Consider adding a zero address check before assigning a state variable.

Update: Acknowledged, not resolved. The team stated:

Incorrect configurations are always a risk, and we can’t check for every possible incorrect configuration. So we acknowledge with no change.

Misleading NatSpec

The NatSpec for the execute function within CounterfactualDepositSpokePool, CounterfactualDepositCCTP, CounterfactualDepositOFT, and WithdrawImplementation are identical in that they are inherited from ICounterfactualImplementation. However, the implementations underlying these execute functions are similar only in their input parameters, while the business logic, revert paths, and parameter encodings are different. This creates uncertainty for integrators, especially since these implementations are interacted with via delegatecall.

Consider expanding the NatSpec of the aforementioned contracts to include an implementation-specific overview to avoid ambiguity.

Update: Resolved in pull request #1349 at commit e7cbd90.

Function Visibility Overly Permissive

Throughout the codebase, multiple instances of functions having overly permissive visibility were identified:

  • The _depositForBurn function in CounterfactualDepositCCTP.sol with internal visibility could be limited to private.
  • The execute function in CounterfactualDepositFactory.sol with public visibility could be limited to external.
  • The _execute function in CounterfactualDepositFactory.sol with internal visibility could be limited to private.
  • The _deposit function in CounterfactualDepositOFT.sol with internal visibility could be limited to private.
  • The _checkFee function in CounterfactualDepositSpokePool.sol with internal visibility could be limited to private.
  • The _verifySignature function in CounterfactualDepositSpokePool.sol with internal visibility could be limited to private.

To better convey the intended use of functions, consider changing a function's visibility to be only as permissive as required.

Update: Resolved in pull request #1348 at commit dd9d6cd.

Lack of Indexed Event Parameters

Throughout the codebase, multiple instances of events not having any indexed parameters were identified:

To improve the ability of off-chain services to search and filter for specific events, consider indexing event parameters.

Update: Resolved in pull request #1346 at commit cde97ee.

Missing Security Contact

Providing a specific security contact (such as an email address or ENS name) within a smart contract significantly simplifies the process for individuals to communicate if they identify a vulnerability in the code. This practice is quite beneficial as it permits the code owners to dictate the communication channel for vulnerability disclosure, eliminating the risk of miscommunication or failure to report due to a lack of knowledge on how to do so. In addition, if the contract incorporates third-party libraries and a bug surfaces in those, it becomes easier for their maintainers to contact the appropriate person about the problem and provide mitigation instructions.

Throughout the codebase, multiple instances of contracts not having a security contact were identified:

Consider adding a NatSpec comment containing a security contact above each contract definition. Using the @custom:security-contact convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.

Update: Resolved in pull request #1351 at commit 63eba2a.

Possible Duplicate Event Emissions

When a setter function does not check if the value being set is different from the existing one, it becomes possible to set the same value repeatedly, creating a potential for event spamming. Repeated emission of identical events can also confuse off-chain clients.

In the AdminWithdrawManager.sol, multiple instances of possible event spamming were identified:

  • The setDirectWithdrawer sets the directWithdrawer and emits an event without checking if the value has changed.
  • The setSigner sets the signer and emits an event without checking if the value has changed.

Consider adding a check that reverts the transaction if the value being set is identical to the existing one.

Update: Acknowledged, not resolved. The team stated:

Acknowledged with no change

Missing Docstrings

Throughout the codebase, multiple instances of missing docstrings were identified:

Consider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Update: Resolved in pull request #1350 at commit 7eae2f2.

Leaf Parameters Pre-Hash Length Allows Ambiguity

The CounterfactualDeposit contract's execute function computes the Merkle leaf as: leaf = keccak256(abi.encode(implementation, keccak256(params))). The abi.encode(address, bytes32) is 64 bytes before hashing. The MerkleProof library warns against 64-byte leaf preimages because internal nodes are also hashed from 64 bytes (bytes32 || bytes32), creating leaf/internal-node ambiguity in generic constructions.

In the CounterfactualDeposit contract, the leaf preimage is structured (address word + keccak256(params)), so practical abuse is infeasible. However, for further hardening consider adding explicit leaf domain separation (for example, prefix or double-hash leaves) and keep off-chain tree construction aligned to remove leaf/internal-node ambiguity by construction.

Update: Resolved in pull request #1352 at commit 7b28c62. 

Conclusion

The diff audit covered several changes to the Across protocol contracts. The primary focus was the addition of counterfactual deposit contracts (including admin and withdrawal related components thereof), which introduce a deterministic "deposit first, deploy later" architecture built around CREATE2/EIP-1167 clone deployments and backend-specific execution paths for SpokePool, CCTP, and OFT bridging. In addition, the changes also included a new Transfer Proxy to support same chain swaps, a Tron-compatible address derivation library, and updates to the OpenZeppelin imports used.

The reviewed changes generally show a clear separation of concerns between factory deployment, shared base logic, and bridge-specific behavior, which improves maintainability and reduces cross-component coupling.

A medium-severity issue was identified which is caused by the relayer having independent control over deposit flow parameters which may run counter to user intentions - allowing for maximal fee extraction or destination-chain selection under certain circumstances. Several low-severity issues and notes were also shared which pertain to code or documentation improvements. There remains an opportunity to harden signature-based authorization and improve documentation around authorization and replay assumptions.

The Across team is appreciated for their responsiveness during the audit. 

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.