Introduction
A recent security audit conducted by OpenZeppelin focused on the Across protocol's contract repository, specifically examining the SpokePoolPeriphery contract and its associated components. The audit identified several vulnerabilities across high, medium, and low severity levels, primarily concerning smart contract swap logic, signature handling, EIP-712 encoding, and replay attacks. All identified issues have been successfully resolved. This comprehensive review aimed to enhance the security and efficiency of the Across protocol's cross-chain bridging functionality, ultimately improving the user experience for asset transfers.
Executive Summary
The audit was a differential analysis targeting specific changes in the codebase. The scope included a detailed review of new peripheral smart contracts designed to augment the Across V3 ecosystem. The audit timeline spanned from May 15, 2025, to May 26, 2025. The code was written in Solidity.
A total of 13 issues were discovered and subsequently resolved:
- High Severity: 1
- Medium Severity: 3
- Low Severity: 3
- Notes & Additional Information: 6
This demonstrates a proactive and effective response from the development team in addressing security concerns.
Audit Scope and Methodology
OpenZeppelin performed a differential audit on the across-protocol/contracts repository. The baseline for comparison was commit 7362cd0 (master), and the head version under review was commit b84dbfa.
The following files and directories were within the audit's purview:
contracts
├── external
│ └── interfaces
│ ├── IERC20Auth.sol
│ └── IPermit2.sol
├── handlers
│ └── MulticallHandler.sol
├── interfaces
│ └── SpokePoolPeripheryInterface.sol
├── libraries
│ └── PeripherySigningLib.sol
└── SpokePoolPeriphery.solSystem Overview
The Across protocol is a cross-chain bridge designed for the fast and cost-effective transfer of ERC-20 tokens and native assets across various networks. Users (depositors) lock assets on a source chain, which are then provided to them on a destination chain by relayers who advance their own funds. The protocol reimburses relayers using funds available on the chosen chain or, if sufficient funds are unavailable on a specific chain, from the HubPool on Ethereum.
This audit centered on a new set of peripheral smart contracts intended to boost the functionality, flexibility, and user experience when interacting with the Across V3 ecosystem.
SpokePoolPeriphery
The SpokePoolPeriphery contract acts as a user-friendly entry point to the Across protocol, significantly expanding the options for initiating cross-chain transfers. Its core functionalities include:
- Swap and Bridge: This flagship feature allows users to bridge assets even if they don't hold the specific token required by the SpokePool. The contract can take a user-specified
swapToken, execute a trade on a designated external exchange to convert it into an Across-acceptedinputToken, and then initiate the bridge deposit—all within a single atomic transaction. This feature supports proportional scaling of outputs, meaning if the swap yields moreinputTokenthan the user's specified minimum, the quantity of tokens received on the destination chain can be proportionally increased. Versatile Token Authorization: The contract integrates multiple industry-standard mechanisms for authorizing the transfer of user tokens, offering flexibility and gas-efficient interactions:
- Standard ERC-20
approve+transferFrom - Native currency (e.g., ETH) deposits, where a non-zero
msg.valueis automatically wrapped into its WETH equivalent - EIP-2612
permit - Permit2 (
permitWitnessTransferFrom) for batched approvals and more advanced signature-based allowances via the canonicalPermit2contract - EIP-3009
receiveWithAuthorizationfor tokens supporting this ERC-20 extension
- Standard ERC-20
- Isolated Swap Execution via
SwapProxy: To enhance security and modularity, all swap operations are delegated to a dedicatedSwapProxycontract. TheSpokePoolPeripherydeploys this proxy and transfers tokens to it for swapping. TheSwapProxythen handles token approvals to the specified exchange or the Permit2 contract and executes the swap calldata on the target exchange. Finally, the output tokens are transferred back to theSpokePoolPeripherycontract.
MulticallHandler Changes
Changes to the MulticallHandler contract included the addition of a makeCallWithBalance function. This function can be used to populate given calldata with the MulticallHandler contract's balance of a specified token and then call a target contract with this modified calldata. This is particularly useful when the amount of tokens arriving on the destination chain is unknown at the time the calldata is specified, a scenario that can occur when using the Swap and Bridge functionality from the SpokePoolPeriphery contract, where the depositor is unaware of the swap's output amount when signing the deposit data.
It is the depositor's responsibility to provide the correct token and offset within which the balance should be populated, keeping in mind that balances can be represented with types smaller than uint256. Specifying an incorrect offset could lead to unintended consequences, such as loss of funds. Users should also note that the makeCallWithBalance function is not suitable for exchanges that require a negative token amount as a parameter, as it can only populate calldata with a non-negative balance.
PeripherySigningLib
The PeripherySigningLib is a library that supports the signature-based functionalities of the SpokePoolPeriphery. Its contributions include:
- Standardized Hashing: Provides functions to compute EIP-712 compliant typed data hashes for the
BaseDepositData,Fees,DepositData, andSwapAndDepositDatastructs. This ensures consistent and secure signature generation and verification. - Signature Deserialization: Offers a utility function to parse a raw bytes signature into its
v, r, scomponents, simplifying signature handling within the main contract logic.
Updated Security Model and Trust Assumptions
The introduction of these peripheral contracts expands the functionality of the Across protocol and, consequently, introduces new elements to its security model and specific trust assumptions:
- The
swapAndBridgefunctionality relies on external exchanges specified by the user (or a trusted frontend). The safety of user funds during the swap depends on the security of the chosen exchange and the integrity of the providedrouterCalldata. A compromised exchange or malicious calldata could result in a loss of funds. - Users (or frontends acting on their behalf) are responsible for the correctness and security of parameters such as the exchange address for the swap, router calldata,
MulticallHandlerinstructions, and EIP-712 signed messages, as incorrect or malicious inputs can lead to transaction failure, loss of funds, or unintended interactions. It is assumed that users only use trusted exchanges, specify reasonable minimum token output amounts, and provide correct EIP-712 signatures. - The use of Permit2 implies trust in the security and operational integrity of the canonical
Permit2contract. It is assumed this contract functions correctly. It is also important to note that theSpokePoolPeripherycontract relies on the existence of thePermit2contract on the chain where it is deployed. - Users are responsible for submitting correct swap data, including, but not limited to, specifying reasonable minimum swap output token amounts and deposit output quantities.
- Submitters (e.g., relayers) should always simulate signed swap transactions offline before submission. Because the
swapProxyblindly calls the user-specifiedexchangewith arbitraryrouterCalldata, a malicious signer could point it to a contract that, for example, enters an infinite loop or performs a return bomb attack, consuming all gas before reverting. Without simulation, the relayer would bear the full gas cost of the failed call and receive no compensation.
👉 Explore advanced security strategies for smart contracts
Detailed Findings and Resolutions
High Severity Issues
Incorrect Nonce Passed to Permit2.permit Function
The performSwap function in the SwapProxy contract allows providing tokens for the swap to the specified exchange using several different methods. Notably, it allows approving tokens for the swap via the Permit2 contract. To do this, it approves the given token amount to the Permit2 contract and then calls the permit function of the Permit2 contract.
However, the nonce specified for this call was global to the entire contract, whereas the Permit2 contract stores a separate nonce for each (owner, token, spender) tuple. Consequently, any attempt to use a different (token, spender) pair than the one used in the first performSwap function call would revert due to a nonce mismatch.
Resolution: The fix involved storing and using a separate nonce for each (token, spender) pair within the SwapProxy contract. This was implemented in pull request #1013.
Medium Severity Issues
Potential for Replay Attacks on SpokePoolPeriphery
The SpokePoolPeriphery contract allows users to deposit or swap and deposit tokens into the SpokePool. Assets are first transferred from the depositor's account, optionally swapped into a different token, and finally deposited into the SpokePool.
Assets can be transferred from the depositor's account in several ways, including methods that require additional user signatures (ERC-2612 permit, Permit2, ERC-3009 receiveWithAuthorization). These signed actions could be executed by anyone on behalf of a user. The signature data for deposits or swap-and-deposits using ERC-2612 permit and ERC-3009 receiveWithAuthorization did not contain a nonce, meaning these signatures could be replayed later.
An attacker could use an old signature containing a smaller token amount than a newer approval, forcing the victim to execute an unexpected swap or bridge tokens to a different chain than intended.
Resolution: A permitNonces mapping was added, and the SwapAndDepositData and DepositData structs were extended with a nonce field. The contract now validates and increments the nonce before verifying the EIP-712 signature in the relevant functions, ensuring each permit-based operation can only be executed once. This was addressed in pull request #1015.
Possible Denial-of-Service (DoS) on Swap via Permit2
The performSwap function in the SwapProxy contract allows the caller to execute a swap by approving or sending tokens to the specified exchange or by approving tokens via the Permit2 contract. However, because any address can be provided as the exchange parameter and any call data can be provided via the routerCalldata parameter, it was possible to force the SwapProxy contract to execute an arbitrary call to any address.
An attacker could exploit this to force the SwapProxy to call the invalidateNonces function on the Permit2 contract, specifying an arbitrary spender and a nonce higher than the current one. This would update the nonce for that (token, spender) pair. A subsequent call to performSwap would then fail because it would try to use a nonce that had been invalidated by the attacker, permanently preventing swaps for that specific (token, exchange) pair.
Resolution: The fix was to disallow the exchange parameter from being equal to the Permit2 contract address. This was implemented in pull request #1016.
Incorrect EIP-712 Encoding
The PeripherySigningLib library contains EIP-712 encoding for certain types and helper functions for generating EIP-712 compliant hash data. However, the data type for the SwapAndDepositData struct was incorrect because it contained a TransferType member, which is an enum type not supported by the EIP-712 standard.
Resolution: The enum name used for generating the SwapAndDepositData struct data type was replaced with uint8 to ensure EIP-712 compliance. This was fixed in pull request #1017.
Low Severity Issues
deposit Function Incompatible with Non-EVM Destination Chains
The deposit function in the SpokePoolPeriphery contract allowed users to deposit native value into the SpokePool. However, its recipient and exclusiveRelayer parameters were of type address and were cast to bytes32. This made it impossible to bridge wrapped native tokens to non-EVM blockchains.
Resolution: The type of the recipient and exclusiveRelayer parameters in the deposit function was changed to bytes32 to allow callers to specify non-EVM addresses for the deposit. This was resolved in pull request #1018.
Integer Overflow in _swapAndBridge
In the _swapAndBridge function, the adjusted output amount was calculated as the product of depositData.outputAmount and returnAmount divided by minExpectedInputTokenAmount. If depositData.outputAmount * returnAmount exceeded 2^256–1, the transaction would revert immediately at the multiplication step, even if the final division result was within acceptable limits. This intermediate overflow was not visible to users, who would only see a generic failure.
Resolution: The potential for overflow in this specific scenario was documented to improve clarity for developers and users. This was addressed in pull request #1020.
Inflexible Fee Recipient Field Preventing Open Relaying
Each DepositData and SwapAndDepositData payload had to contain a hardcoded fee recipient address, and the periphery would pay the submission fee to that exact address upon a successful deposit or swap-and-bridge. While this ensured users knew exactly who would receive their fees in advance, it also prevented open relayer competition or fallback options if the chosen relayer was underperforming or unavailable.
Resolution: The explicit fee recipient field option was retained in the data structures, but a "zero address" convention was introduced. If the fee recipient is the zero address, the periphery defaults to using msg.sender as the payee. This enables open relaying. This change was implemented in pull request #1021.
Frequently Asked Questions
What was the primary goal of this audit?
The audit aimed to identify potential security vulnerabilities and code quality issues in new peripheral contracts (SpokePoolPeriphery, MulticallHandler, PeripherySigningLib) added to the Across protocol, ensuring the safe enhancement of its cross-chain bridging capabilities.
Were all the discovered issues fixed?
Yes, the Across protocol team addressed all 13 findings identified by OpenZeppelin, including one high-risk and three medium-risk vulnerabilities, prior to deployment.
What is the most significant security improvement made?
A critical fix was implemented for the Permit2 nonce handling in the SwapProxy, which prevented a flaw that could have blocked swap functionality for specific token and spender pairs.
How does the swapAndBridge feature improve user experience?
It allows users to bridge assets even if they don't directly hold the required input token by automatically swapping a different token in the same transaction, making the bridging process more flexible and user-friendly.
What should users know about the updated security model?
Users must trust the security of the external exchanges they specify for swaps and ensure the integrity of the calldata provided. The system also introduces trust in the canonical Permit2 contract's security.
Is my interaction with the protocol now safer?
Yes, the successful resolution of all audit findings significantly enhances the overall security posture of these new features within the Across protocol ecosystem.
Additional Notes and Optimizations
Beyond the security findings, the audit report included several recommendations for improving code quality, documentation, and gas efficiency:
- Function Renaming: The
depositfunction was suggested to be renamed todepositNativefor better clarity, as it only handles native currency deposits. This was implemented. - Code Optimizations: Several gas optimization opportunities were identified, such as removing redundant checks and simplifying string literals.
- Documentation Enhancements: Instances of insufficient, misleading, or outdated documentation were highlighted and subsequently corrected by the team. This included clarifying the purpose and limitations of the
makeCallWithBalancefunction and adding missing NatSpec comments. - Typos and Unused Code: Minor typographical errors were corrected, and unused code segments (like an unused error definition and an unused import) were removed to improve code cleanliness and maintainability.
👉 View real-time tools for secure blockchain development
Conclusion
The reviewed changes to the periphery contracts introduce new possibilities for depositing assets into the SpokePool. They enable third-party entities to deposit or swap-and-deposit funds on behalf of any user providing a valid signature. Furthermore, they safeguard users from losing their native tokens when specifying an incorrect SpokePool address for a deposit.
While the audit uncovered several issues related to swap logic and signature handling, the code was found to be robust and well-structured. The responsive and thorough approach of the development team in addressing all findings has resulted in a more secure and reliable system for users engaging in cross-chain transactions through the Across protocol.