- Table of Contents
- Summary
- Scope
- System Overview
- Security Model and Trust Assumptions
- Privileged Roles
- Medium Severity
- Low Severity
- [Across] Error-Prone Initialization of Blast_Adapter.sol
- [Across] Permit2 Witness Is Not Fully Compliant With EIP-712
- [Across] Users May Lose Assets if They Specify an Empty Address as a Call Target
- [Across] Blast_SpokePool May Fail on Attempt to Bridge Native Blast Tokens
- [Oval] PermissionProxy.execute Will Fail if Non-Zero value Is Provided
- [Oval] CoinbaseSourceAdapter May Return Old Prices
- [Oval] Missing Input Validation
- Notes & Additional Information
- [Across] Lack of Indexed Event Parameters
- [Across] Unnecessary Cast
- [Across] Variable Names Inconsistent With Interfaces
- [Across] Code Duplication
- Lack of Security Contact
- [Oval] State Variable Visibility Not Explicitly Declared
- [Across] Orders May Be Blocked for Some Time
- [Across] Multiple Contract Declarations Per File
- [Across] Typographical Errors
- [Oval] Typographical Errors
- [Across] Prefix Increment Operator ++i Can Save Gas in Loops
- [Across] Misleading Comments
- [Oval] Misleading Comment
- [Oval] Possible Duplicate Event Emission
- Conclusion
Summary
- Type
- DeFi
- Timeline
- From 2024-05-20
- To 2024-06-07
- Languages
- Solidity
- Total Issues
- 25 (24 resolved, 1 partially resolved)
- Critical Severity Issues
- 0 (0 resolved)
- High Severity Issues
- 0 (0 resolved)
- Medium Severity Issues
- 4 (4 resolved)
- Low Severity Issues
- 7 (7 resolved)
- Notes & Additional Information
- 14 (13 resolved, 1 partially resolved)
Scope
We audited the across-protocol/contracts repository at commit 95c4f92.
In scope were the following files:
contracts
├── Blast_SpokePool.sol
├── chain-adapters
│ ├── Blast_Adapter.sol
├── erc7683
│ ├── ERC7683.sol
│ ├── ERC7683Across.sol
│ └── ERC7683Depositor.sol
├── handlers
│ ├── MutlicallHandler.sol
└── upgradeable
└── MultiCallerUpgradeable.sol
In addition, the following PRs have been audited:
We also audited the UMAProtocol/oval-contracts repository at commit 297ab25.
In scope were the following files:
src
├── adapters/source-adapters
│ ├── CoinbaseSourceAdapter.sol
├── controllers
│ ├── MutableUnlockersController.sol
├── factories
│ ├── BaseFactory.sol
│ ├── PermissionProxy.sol
│ ├── StandardChainlinkFactory.sol
│ ├── StandardCronicleFactory.sol
│ ├── StandardCoinbaseFactory.sol
│ ├── StandardPythFactory.sol
└── oracles
└── CoinbaseOracle.sol
In addition, the following pull requests have been audited
System Overview
Across
Across is a cross-chain bridge acceleration protocol designed to facilitate the rapid transfer of tokens between Ethereum Layer 1 (L1) and the supported Layer 2 (L2) chains, as well as between L2 chains. Users of the protocol deposit funds to be transferred to their desired destination chain. Subsequently, third-party entities known as relayers fulfill the bridge orders by transferring their own funds to the specified destination address. Depositors receive the bridged amount, minus a system fee, while relayers are reimbursed by the protocol and additionally compensated with a reward for their services.
A SpokePool
contract is deployed on each supported chain, with the HubPool
contract on the L1 chain managing the overall liquidity within the system. The HubPool
contract either transfers funds to the SpokePool
contracts to facilitate relayer refunds or requests the return of excess funds from the SpokePool
contracts back to HubPool
. For a more detailed description of the protocol, you may refer to one of our previous reports.
Summary of Changes
During this audit, some new contracts added to the codebase are considered. These additions mainly concern:
- Blast L2 integration. Blast is an OP-stack-based chain providing native yield for Ethereum and certain stablecoins. The
Blast_Adapter
contract facilitates the bridging of funds from L1 to Blast, while theBlast_SpokePool
contract enables the bridging of funds from Blast to Ethereum L1 or other L2 chains supported by the protocol. Both contracts have been audited in this assignment. - Compliance with EIP-7683. EIP-7683 establishes a standardized API for cross-chain trade execution systems. It provides a generic
CrossChainOrder
struct, as well as a standardISettlementContract
smart contract interface. The Across protocol implements this standard while also being in full compliance with EIP-712. - The
MutlicallHandler
contract allows a user to specify a series of arbitrary calls that should be made upon receiving bridged funds via the message field in the Across deposit transaction. - Function
depositExclusive
in theSpokePool
contract, which allows depositors to specify a fill deadline offset instead of a fill deadline timestamp. The offset value is added to the deposit transaction's timestamp to produce the final fill deadline.
Oval
Oval is a system designed to be used by the protocols which create MEV opportunities as a result of oracle price updates. Such opportunities are referred to as Oracle Extractable Value (OEV) and may arise in case of a price change that can cause liquidation in a DeFi protocol.
Oval introduces a mechanism which allows such protocols to capture some part of the OEV they generate. It involves providing intermediary smart contracts which may be plugged in between a DeFi protocol and the oracle being used. Such an intermediary contract fetches the data from an oracle and provides it to the target protocol. The price data to be provided may only be made available by a certain set of unlockers. A transaction which makes a price available to the target DeFi protocol may be back-run in order to extract OEV, for example, by liquidating a loan which became unhealthy after the price update.
These "price unlock" transactions are not submitted directly to the mempool. Instead, some information about them is shared in Oval nodes which run an auction for the right to be the first to use the price update. The transaction of the user who won the auction is then appended directly to the unlock transaction and passed to the Flashbots MEV-Share Node which, upon a successful execution of the received transaction bundle, refunds some percentage of funds paid for executing the bundle. This refund amount is split between UMA and the protocol integrating with it. More information about Oval may be found in one of our previous reports.
Summary of Changes
This audit focused on the code newly added to the protocol rather than on the entire codebase. The changes and additions which have been audited include:
- Addition of the Coinbase API oracle and the Coinbase adapter allowing the Coinbase oracle to be used in Oval
- Support for the Chainlink
roundId
parameter in the exposed data from the oracles - Addition of the factories deploying standard Oval instances for Chainlink, Chronicle, Coinbase, and Pyth
- Addition of the
PermissionProxy
contract which, when set as a price unlocker, allows multiple whitelisted users to act as unlockers - Addition of the
MutableUnlockersController
contract, allowing the set of unlockers to be changed after the initial set-up - Introduction of the
maxAge
parameter limiting the staleness of the price data reported by the adapters
Security Model and Trust Assumptions
Across
The protocol uses the canonical bridges of the supported L2 chains in order to refund the relayers and relies on their infrastructure and functionality. By integrating the Blast L2 chain, its canonical bridge is also considered to work as expected. Across implements EIP-7683 and is expected to be used by external cross-chain execution systems. It is assumed that these external systems work as expected and behave in the best interest of their users. No further trust assumptions are introduced to the protocol with the new contracts audited in this assignment.
Oval
Oval relies on both on-chain and off-chain trust assumptions. The off-chain trust assumptions include:
- Oval nodes process bundles and set refund instructions correctly.
- Flashbots MEV-Share system does not un-bundle or leak MEV from back-runs.
- Block builders respect MEV-Share's building rules.
In order for the Oval system to function correctly from the on-chain perspective, it is important that the unlockers frequently send the "unlock" transactions making the newest prices available. Moreover, it is trusted that the unlockers behave in the best interest of the protocol and its users, which includes not leaking the "unlock" transactions outside the Oval nodes and not utilizing them in order to extract OEV for themselves.
Furthermore, the newest update introduced the CoinbaseOracle
contract which is responsible for fetching the price data from the Coinbase oracle to the protocols integrating with Oval. Since the Coinbase oracle is not deployed on-chain, it is essential that the prices provided by it are delivered to the CoinbaseOracle
on a regular basis. Otherwise, stale prices may be returned.
Privileged Roles
Across
No further privileged roles have been introduced with the newly added contracts audited in this assignment.
Oval
In the Oval system, there are several privileged roles:
- Updaters: Entities capable of unlocking the latest prices reported by the oracles.
- Owner of the
BaseController
andMutableUnlockersController
contracts: An account entitled to modifying the unlockers set and changing important parameters such aslockWindow
,maxTraversal
, andmaxAge
. - Owner of the
PermissionedProxy
contract: In case thePermissionedProxy
contract is set as an unlocker, the owner may grant unlocking rights to additional accounts. - Owner of the Factory Contracts: Apart from the privileges of the owner of the controller contracts, the owner may additionally specify the set of default unlockers for each deployed controller instance.
It is trusted that each of these entities will behave in the best interests of the protocol and its users.
Medium Severity
[Across] It Will Not Be Possible to Bridge DAI to Blast
The relayTokens
function of the Blast_Adapter
contract is responsible for bridging tokens from Ethereum to Blast. In order to do that, it uses either the L1 standard bridge or the L1 Blast bridge, depending on the token being bridged. In case of DAI, an attempt to call depositERC20To
function of the L1 Blast bridge is made. However, the bridge does not contain this function. This means that all transactions trying to bridge DAI to Blast using the relayTokens
function will revert.
When DAI is being bridged, consider calling the bridgeERC20To
function of the L1 Blast bridge instead.
Update: Resolved in pull request #518 at commit 9b9b3d6.
[Oval] Attempts to Push Price to the CoinbaseOracle
Will Always Fail
The CoinbaseOracle
contract serves as an oracle for price data reported and signed by Coinbase. In order to supply such price data to the contract, users have to call the pushPrice
function which, in case of the correct data passed, updates the price data for a given asset. In order to verify the correctness of provided data, several checks are made, including the check for a valid kind
field. However, this check reverts when kind
is different than "price". The data signed by the Coinbase oracle will always contain kind
equal to "prices", resulting in the pushPrice
function always reverting, making it impossible to provide asset prices.
Consider verifying that the provided kind
value is equal to "prices".
Update: Resolved in pull request #18 at commit ff31b1b.
[Oval] Newest Prices May Be Reported Ignoring the lockWindow()
Constraint
The _tryLatestRoundDataAt
function is invoked after the tryLatestDataAt
function is called inside the internalLatestData
function of the Oval
contract. Its timestamp
argument is equal to the maximum of the last unlock time and block.timestamp - lockWindow()
. Before the introduction of the maxAge()
parameter, it used to be that in cases where the price data had been unlocked inside the lockWindow()
period, the price data would be returned. In the opposite case, the most recent price as of block.timestamp - lockWindow()
timestamp would be returned.
Now, the maxAge()
parameter has been introduced to limit the staleness of the data returned by the adapters. If the data to be returned by the _tryLatestRoundDataAt
function is older than the maxAge()
, the most recent price data is returned instead. This means that if the maxAge()
is lower than the lockWindow()
and the last price is unlocked within the lockWindow()
, but its timestamp is older than the maxAge()
parameter, the newest price will be returned by the internalLatestData
function of the Oval
contract.
However, this is not the desired behavior as the lock window mechanism has been introduced in order to guarantee that the "unlock" transactions unlock the most recent price that has not yet been used by anyone. As a consequence, the OEV opportunity for Oval will be lost since the newest price will be available for use by anyone before the "unlock" transaction takes place. A similar scenario may happen if the maxTraversal
parameter is set to 0. In such a case, uninitialized data is returned by the _searchRoundDataAt
functions inside the adapters and hence the check against the maxAge()
value will never pass. As a result, the newest price data will always be returned.
Consider carefully validating the parameters with which the Oval
contracts are deployed, especially ensuring that the max age is always bigger than the lock window and that the maxTraversal
parameter is set to a reasonable value.
Update: Resolved in pull request #17 at commit 2aa2c7d.
[Oval] Updating Parameters Inside the BaseController
Contract May Lead to Undesired Outcomes
The maxAge()
parameter has been introduced to controllers in order to limit the staleness of the data returned by the adapters. Stale data could possibly be returned if no "unlock" transaction happens during the lockWindow()
period. In such a case, the latest available price data for block.timestamp - lockWindow()
is returned. The maxAge()
parameter specifies the maximum age of such price data and, in case when the price is too old, the most recent price data is returned. Inside both ImmutableController
and MutableUnlockersController
, the maxAge()
parameter is immutable and cannot be changed, but it is possible to change it inside the BaseController
contract.
However, changing this parameter may lead to undesired results. One of the problems which may happen after such a change arises when the price data from an oracle is not unlocked for the lockWindow()
time and the timestamp of the most recent price older than the lockWindow()
is older than what the maxAge()
parameter allows. In such a case, the newest price data is returned in order to avoid returning too stale prices. However, if maxAge()
is then changed to a higher value, such that the most recent price as of block.timestamp - lockWindow()
is not older than the maxAge()
, that older price will be returned. It means that in the same block, the oracle adapter may return the most recent data and then the stale data, which is an unexpected and incorrect behavior.
The opposite scenario is also possible when the maxAge()
parameter is decreased. In such a case, the most recent data will be unexpectedly returned which will take away the possibility of extracting OEV for the newest price for Oval by unlocking the price. It should be noted that these are just two of many different scenarios that may happen if the maxAge()
parameter is changed. Similar problems may also arise when the unlockWindow()
and maxTraversal()
parameters are modified, even if maxAge()
stays the same.
Consider setting the maxAge()
, unlockWindow()
, and maxTraversal()
parameters as immutable inside the BaseController
in order to avoid returning multiple different prices for the same timestamp.
Update: Resolved in pull request #19, at commit 0e94235. It is ensured that the reported price does not change when any of the lockWindow
, maxAge
and maxTraversal
parameters is changed and also that maxAge
is always a value greater than lockWindow
.
Low Severity
[Across] Error-Prone Initialization of Blast_Adapter.sol
The constructor of the Blast_Adapter
contract initializes the inherited CircleCCTPAdapter
contract by providing the Circle domain ID of the Base chain. However, Blast is not yet supported by the CCTP protocol. The CircleCCTPAdapter
library contract is currently inherited by the Blast_Adapter
contract only in case of future use (when Blast integrates with CTTP). Thus, CircleCCTPAdapter
should be dummy initialized at the moment.
Consider clearly initializing all CircleCCTPAdapter
parameters with dummy values in Blast_Adapter
to avoid possible erroneous configuration in a future version of the contract.
Update: Resolved in pull request #525.
[Across] Permit2 Witness Is Not Fully Compliant With EIP-712
The ERC7683Permit2Lib
library contains EIP-712 typehashes of various types and helper functions for hashing data. However, the hashOrder
and hashOrderData
functions, which are used to calculate the Permit2 witness, do not comply with the EIP-712 standard as they use abi.encodePacked
for encoding data which will not pad each member value to 32 bytes as required by the EIP-712 standard. As such, abi.encode
should be used instead.
Moreover, since the message
member of the AcrossOrderData
struct is of type bytes
, its keccak256
hash should be used for the encoding. However, the message
member is used directly instead.
Additionally, the data passed to the permitWitnessTransferFrom
function as the witnessTypeString
argument should contain the custom witness declaration followed by encodings of all nested structs and the TokenPermissions
struct in the alphabetical order. The reason for that is that both the custom witness and the TokenPermissions
structs are members of the resulting PermitWitnessTransferFrom
struct, whose encoding is then used for verifying the signature. As a result, both the TokenPermissions
struct and all nested structs of the witness become the referenced struct types and because of that, should be sorted by name in the encoding of the PermitWitnessTransferFrom
struct. However, the data passed to the permitWitnessTransferFrom
does not follow this order.
Consider updating the hashOrder
and the hashOrderData
functions so that they return data that is compliant with EIP-712. Moreover, consider changing the order of the struct encodings in the data passed to the permitWitnessTransferFrom
function, so that they are sorted alphabetically.
Update: Resolved in pull request #519 at commit 0dbde21 and in pull request #506 at commit e2d406f.
[Across] Users May Lose Assets if They Specify an Empty Address as a Call Target
The attemptCalls
function of the MulticallHandler
contract enables users to specify and execute custom operations on a target blockchain right after tokens are bridged. It performs these operations by doing a series of low-level calls to the target addresses with the given data. If any of these calls fails, the entire chain of transactions is reverted in order to protect users from losing their assets.
In Solidity, whenever high-level calls are made to an address without any code, the execution reverts. However, when low-level calls are made to an empty address, they are considered successful. It means that if a user mistakenly specifies an address not containing any code as a target, their transaction will succeed, whereas it would have failed if high-level calls had been made. This may cause users to lose their assets.
In order to illustrate this, consider an example where a user bridges token A
from Ethereum to Blast and wants to immediately swap it to token B
and send the received amount of B
to their own account. In addition, suppose that the DEX they want to use has different address on these two blockchains. If a user mistakenly specifies the DEX address from Ethereum, the call made to this address (with no code) on Blast will be treated as successful and the subsequent transaction will send zero amount of B
tokens to the user's account. All bridged A
tokens will still remain in the contract, available to be stolen by anyone.
Consider validating the code size of a target address for each call with non-empty calldata in the attemptCalls
function and revert in case it equals 0.
Update: Resolved in pull request #531 at commit 53d21b6.
[Across] Blast_SpokePool
May Fail on Attempt to Bridge Native Blast Tokens
The _bridgeTokensToHubPool
function handles bridging tokens from the Blast spoke pool to the hub pool. For each token different from USDB and WETH, the withdrawTo
function of the standard bridge is called. However, this function will only work for ERC-20 tokens that are not native to Blast, as otherwise, the standard bridge will attempt to call the l1Token
function which may not be implemented for native tokens and the transaction will revert. Furthermore, in the case of tokens native to Blast, it is expected that a sufficient allowance is given beforehand to the bridge as it calls safeTransferFrom
in order to lock the tokens. However, currently, the Blast_SpokePool
has no mechanism to approve tokens to the bridge.
Consider approving the relevant amount of tokens to the bridge and calling the bridgeERC20To
function of the standard bridge (as described here) when bridging native Blast ERC-20 tokens from the spoke pool. Alternatively, if tokens native to Blast are not expected to be bridged in the future, consider documenting it.
Update: Resolved in pull request #520 and in pull request #536. The UMA team stated:
We decided to add a mapping to the
Ovm_SpokePool
to support native L2 token bridging. This will allow OVM chains like Blast to specify the corresponding remote L1 tokens.
[Oval] PermissionProxy.execute
Will Fail if Non-Zero value
Is Provided
The PermissionProxy
contract is intended to be used as a proxy for calls that unlock new prices reported as a part of the MEV-share auction flow. It allows multiple different accounts to unlock new prices by performing calls to the controllers on their behalf. Calls may only be performed by authorized accounts and may be executed by invoking the execute
function. Apart from the call target and the call data, it is possible to also specify the call value. However, the PermissionProxy
contract has no function allowing it to receive the native value. This means that it is not possible for users to execute any call involving non-zero native value as all such calls would revert.
Consider making the execute
function payable
or removing the value
argument if the PermissionProxy
contract is not expected to make any calls with non-zero native value.
Update: Resolved in pull request #20 at commit 77e5441.
[Oval] CoinbaseSourceAdapter
May Return Old Prices
In order to limit the staleness of the data returned by source adapters, the maxAge
function has been introduced. It defines the maximum age of prices that can be returned by the tryLatestDataAt
function. However, the CoinbaseSourceAdapter
does not use the maxAge
function when it returns historical data as other adapters do. This means that it can possibly return very old prices.
Consider limiting the staleness of the data returned by the CoinbaseSourceAdapter
by utilizing the maxAge
function.
Update: Resolved in pull request #24 at commit 70e5b66.
[Oval] Missing Input Validation
The deployer of the CoinbaseOracle
contract sets the immutable reporter
variable during the contract's construction. The reporter
is the sole entity authorized to submit new prices to the oracle via the pushPrice
function. The access control mechanism in pushPrice
involves recovering the signer of the provided message and verifying that it matches the reporter's address. However, the ecrecover
precompile does not fail on invalid signatures. Instead, it returns the zero address. If the reporter
address is erroneously configured to the zero address, it becomes trivial for any entity to push arbitrary prices to the oracle contract.
Consider implementing a validation check to ensure that the reporter address is not set to the zero address to prevent erroneous contract deployments.
Update: Resolved in pull request #18 at commit b057756.
Notes & Additional Information
[Across] Lack of Indexed Event Parameters
Within MulticallHandler.sol
, several events do not have indexed parameters:
- The
CallsFailed
event - The
DrainedTokens
event
To improve the ability of off-chain services to search and filter for specific events, consider indexing event parameters.
Update: Resolved in pull request #532 at commit e2b80bd.
[Across] Unnecessary Cast
Within the Blast_Adapter
contract, the IL1ERC20Bridge(L1_BLAST_BRIDGE)
cast is unnecessary.
To improve the overall clarity, intent, and readability of the codebase, consider removing the unnecessary cast.
Update: Resolved in pull request #530 at commit 983531d.
[Across] Variable Names Inconsistent With Interfaces
Throughout the codebase, there are several instances of variables having different names than those in the original interface defining them:
- The
mode
variable from theIERC20Rebasing
has a different name than the corresponding variable declared in theconfigure
function of the USDB implementation contract deployed on the Blast blockchain. - The
recipient
variable from theIBlast
interface has a different name than the corresponding variable declared in theBlast
implementation contract deployed on the Blast blockchain. - The
settlerContract
variable used for the calculation ofCROSS_CHAIN_ORDER_TYPE
has a different name than its counterpart in theCrossChainOrder
struct.
While these inconsistencies do not have any negative impact on the behavior of the code, fixing them would make the code easier to read. As such, consider changing variable names so that they are consistent with the original ones in order to improve the readability of the codebase.
Update: Resolved in pull request #526 at commit 974e2d6.
[Across] Code Duplication
The SpokePool contracts of the chains based on the Optimistic Virtual Machine (OVM-based chains) share a significant amount of code, primarily utilizing the bridge contracts of Optimism. This common logic is encapsulated within the Ovm_SpokePool
contract, from which all OVM-based SpokePools inherit. The _bridgeTokensToHubPool
function in the Ovm_SpokePool
contract handles the process of transferring funds from the L2 chain back to the HubPool
via bridging.
The Blast_SpokePool
contract, which is also OVM-based, inherits from the Ovm_SpokePool
. However, it overrides the _bridgeTokensToHubPool
function, despite retaining the majority of the code from the Ovm_SpokePool
contract. The special handling of the SNX token within the Ovm_SpokePool
appears to necessitate the overriding of the entire _bridgeTokensToHubPool
function, resulting in extensive code duplication.
Consider relocating the part of the code specifically responsible for handling the SNX token to the relevant OVM-based SpokePool contract. Doing this will enable the Blast_SpokePool
contract to invoke the inherited _bridgeTokensToHubPool
function and avoid code duplication.
Update: Resolved in pull request #520, pull request #524. The SNX
specific logic has been relocated to the Optimism_Spokepool
contract and code duplication has been mitigated.
Lack of Security Contact
Providing a specific security contact (such as an email 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, there are contracts that do not have a security contact.
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 #535 at commit 8043c74.
[Oval] State Variable Visibility Not Explicitly Declared
Throughout the codebase, there are state variables that lack explicitly declared visibility:
- The
reporter
state variable inCoinbaseOracle.sol
- The
SOURCE
state variable inStandardCoinbaseFactory.sol
- The
pyth
state variable inStandardPythFactory.sol
For clarity, consider always explicitly declaring the visibility of variables, even when the default visibility matches the intended visibility.
Update: Resolved in pull request #21 at commit 2552ef0 and in pull request #18 at commit b057756.
[Across] Orders May Be Blocked for Some Time
The ERC7683OrderDepositor
contract allows initiating cross-chain orders by providing users' signatures for these orders to the initiate
function. This function is callable by anyone and, apart from the order to be initiated and the user's signature, it also allows a caller to specify the filler data which is then used to fill the exclusive relayer data for the order. The exclusive relayer is an account which is exclusively allowed to fill a certain deposit before the exclusivity deadline passes.
However, in contrast to the specified order, the fillerData
is not validated in any way inside the initiate
function which means that it may contain an arbitrary value. This allows an attacker to specify a random address, making it impossible for the order to be filled until the exclusivity deadline passes.
Consider limiting the exclusivity deadline period so that this attack is not problematic for the users.
Update: Partially resolved in pull request #515. The UMA team stated:
This no longer becomes an issue once pull request #515 is merged because the depositor now includes in their
CrossChainOrder.order bytes
information about anexclusivityDeadlineOffset
. The filler that then callsinitiate()
to bring the deposit on chain can fill in any account they wish as theexclusiveRelayer
but the depositor can protect themselves from malicious relayers by setting a short deadline offset. Moreover, the channel through which the depositor and relayer communicate will presumably be designed to penalize fillers who misbehave and set any part of thefillerData
with griefing intent
[Across] Multiple Contract Declarations Per File
Within ERC7683Depositor.sol
, more than one contract, library, or interface have been declared.
Consider separating the contracts into their own files to make the codebase easier to understand for both developers and reviewers.
Update: Resolved in pull request #527 at commit 74b6c90.
[Across] Typographical Errors
Throughout the codebase, there are several instances of typographical errors:
In the ERC7683.sol
file:
- In line 12, "receive" should be changed to "received".
In the ERC7683Depositor.sol
file:
- In line 26, "quoteBeforeDeadline" should be replaced with "QUOTE_BEFORE_DEADLINE".
In the Blast_Adapter.sol
file:
- In lines 64 and 65, "Base" should be changed to "Blast".
- In line 89, "blast" should be replaced with "standard".
In the MulticallHandler.sol
file:
- In line 52, "drainRemainingTokens" should be changed to "drainLeftoverTokens".
In the Blast_SpokePool.sol
file:
- In line 9, the word "can" is written twice.
Consider fixing all instances of typographical errors in order to improve the readability of the codebase.
Update: Resolved in pull request #529 at commit bfb8596.
[Oval] Typographical Errors
Throughout the codebase, there are several instances of typographical errors:
In the PermissionProxy.sol
file:
- In line 8, the word "allows" should be removed.
- In line 10, the word "oval" should be written starting with a capital letter.
In the MutableUnlockersController.sol
file:
- In line 8, "can be change" should be "can be changed".
Consider fixing all instances of typographical errors in order to improve the readability of the codebase.
Update: Resolved in pull request #23 at commit d977126.
[Across] Prefix Increment Operator ++i
Can Save Gas in Loops
Throughout the codebase, there are multiple opportunities where the subject optimization can be applied:
- The i++ in
MultiCallerUpgradeable.sol
- The i++ in
MultiCallerUpgradeable.sol
- The i++ in
MulticallHandler.sol
Consider using the prefix increment operator (++i
) instead of the postfix increment operator (i++
) in order to save gas. This optimization skips storing the value before the incremental operation as the return value of the expression is ignored.
Update: Resolved in pull request #522 at commit d70155c and in pull request #534 at commit 47e59f7.
[Across] Misleading Comments
The following instances of misleading comments have been identified:
- In
Blast_SpokePool.sol
, the comment inside the_claimYield
function suggests that the native yield received from the yield contract is converted to WETH, but it is not, as ETH is transferred directly to the recipient. - In
Blast_SpokePool.sol
, in this comment, it might not be immediately clear what the word "this" refers to.0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000
is the address of theLegacyERC20ETH
contract. However, it might be understood from the comment that it is the address of the canonical bridge.
Consider correcting the comments in order to improve the overall clarity and readability of the codebase.
Update: Resolved in pull request #528 and pull request #520.
[Oval] Misleading Comment
The following misleading comment has been identified:
In ChainlinkSourceAdapter.sol
, this comment says that if the data returned by the _searchRoundDataAt
function is uninitialized, the current price data is returned. However, the latest price data will also be returned in case the _searchRoundDataAt
function returns initialized data that is older than maxAge()
.
Consider fixing the comment in order to improve the overall clarity and readability of the codebase.
Update: Resolved in pull request #25, at commit 5624e60.
[Oval] Possible Duplicate Event Emission
When a setter function does not check if the value has changed, it opens up the possibility of spamming events indicating that the value has changed when it has not. Spamming the same values can potentially confuse off-chain clients.
Within MutableUnlockersController.sol
, the setUnlocker
function sets the unlockers
state variable and emits an event without checking if the value has changed.
Consider adding a check to revert the transaction if the value remains unchanged.
Update: Resolved in pull request #26, at commit 6f53172.
Conclusion
Across is a protocol enabling rapid token transfers between Ethereum L1 and L2 chains. Users deposit funds, relayers transfer the funds to the destination, and depositors receive the amount minus a fee. The HubPool
contract on L1 manages liquidity, coordinating with SpokePool
contracts on each supported chain. On the other hand, Oval captures Oracle Extractable Value (OEV) generated by price updates in DeFi protocols. It uses intermediary contracts to fetch and provide price data, with transactions being auctioned to control the distribution of the extractable value. New additions in the Oval protocol include the Coinbase API oracle, support for Chainlink roundId
, and contracts for managing unlockers and data staleness.
The audit yielded several medium- and low-severity issues. In addition, various recommendations have been made to avoid possible misconfigurations that could lead to faulty contract deployments and errors in integrating with external infrastructure.
Both Across and Oval teams have provided useful documentation, detailed explanations, and valuable insights during the audit process.