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: 13/175
Findings: 4
Award: $1,343.77
š Selected for report: 0
š Solo Findings: 0
š Selected for report: ether_sky
Also found by: jasonxiale, nobody2018
1165.9616 USDC - $1,165.96
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L88-L101 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L104-L116 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L160-L177
ArbitrumCoreBranchRouter.sol
is inherited from CoreBranchRouter.sol
, and CoreBranchRouter.sol
is inherited from BaseBranchRouter.sol
, because there is an difference between ArbitrumCoreBranchRouter
and CoreBranchRouter
, BaseBranchRouter._transferAndApproveToken
doesn't approve the appropriate permission, which causes BaseBranchRouter.callOutAndBridge
and BaseBranchRouter.callOutAndBridgeMultiple
might not work on ArbitrumCoreBranchRouter
Take BaseBranchRouter.callOutAndBridge as an example:
function callOutAndBridge(bytes calldata _params, DepositInput calldata _dParams, GasParams calldata _gParams) external payable override lock { //Transfer tokens to this contract. _transferAndApproveToken(_dParams.hToken, _dParams.token, _dParams.amount, _dParams.deposit); //Perform call to bridge agent. IBridgeAgent(localBridgeAgentAddress).callOutAndBridge{value: msg.value}( payable(msg.sender), _params, _dParams, _gParams ); }
The function first calls BaseBranchRouter._transferAndApproveToken, and then calls callOutAndBridge
.
Supposed that we're in ArbitrumCoreBranchRouter, ArbitrumCoreBranchRouter.callOutAndBridge
will first calls BaseBranchRouter._transferAndApproveToken and then calls BranchBridgeAgent.callOutAndBridge
In BaseBranchRouter._transferAndApproveToken, the code first transfer hToken/_underlyingToken to address(this), which is ArbitrumCoreBranchRouter.sol, and then calls approve
for _localPortAddress which is ArbitrumBranchPort.sol
function _transferAndApproveToken(address _hToken, address _token, uint256 _amount, uint256 _deposit) internal { // Cache local port address address _localPortAddress = localPortAddress; // Check if the local branch tokens are being spent if (_amount - _deposit > 0) { unchecked { _hToken.safeTransferFrom(msg.sender, address(this), _amount - _deposit); ERC20(_hToken).approve(_localPortAddress, _amount - _deposit); <--------- Here _localPortAddress is approved } } // Check if the underlying tokens are being spent if (_deposit > 0) { _token.safeTransferFrom(msg.sender, address(this), _deposit); ERC20(_token).approve(_localPortAddress, _deposit); <--------- Here _localPortAddress is approved } }
Then BranchBridgeAgent.callOutAndBridge is called, within BranchBridgeAgent.callOutAndBridge
, BranchBridgeAgent._createDeposit is called. and then IPort(localPortAddress).bridgeOut is called.
IPort(localPortAddress).bridgeOut
is defined in BranchPort.sol#L277-L285, and since we're in ARB, so _bridgeOut
will be ArbitrumBranchPort._bridgeOut
function _bridgeOut( address _depositor, address _localAddress, address _underlyingAddress, uint256 _amount, uint256 _deposit ) internal override { //Store Underlying Tokens if (_deposit > 0) { _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit); <------- Approved in function BaseBranchRouter._transferAndApproveToken, } //Burn hTokens if any are being used if (_amount - _deposit > 0) { unchecked { IRootPort(rootPortAddress).bridgeToRootFromLocalBranch(_depositor, _localAddress, _amount - _deposit); } } }
The function ArbitrumBranchPort._bridgeOut
will call IRootPort(rootPortAddress).bridgeToRootFromLocalBranch to transfer hToken, but rootPortAddress is not approved
function bridgeToRootFromLocalBranch(address _from, address _hToken, uint256 _amount) external override requiresLocalBranchPort { if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); _hToken.safeTransferFrom(_from, address(this), _amount); }
VIM
add approve for rootPortAddress
while in ArbitrumCoreBranchRouter
Other
#0 - c4-pre-sort
2023-10-14T11:12:26Z
0xA5DF marked the issue as duplicate of #744
#1 - c4-pre-sort
2023-10-14T11:12:31Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-27T10:02:56Z
alcueca marked the issue as satisfactory
š Selected for report: kodyvim
Also found by: 0xnev, Kow, QiuhaoLi, SpicyMeatball, ast3ros, ayden, bin2chen, chaduke, jasonxiale, minhtrng, nobody2018
112.9294 USDC - $112.93
RootBridgeAgent._createSettlementMultiple's _hasFallbackToggled
will not work because of typo
According to BranchBridgeAgent.lzReceiveNonBlocking, 0x82 is supported as __hasFallbackToggled if true, fallback on execution failure is toggled on in function _execute
But because of wrong calculating in RootBridgeAgent._createSettlementMultiple, which always set _hasFallbackToggled
as 0x02
// Prepare data for call with settlement of multiple assets _payload = abi.encodePacked( _hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02), <-------------- Here _recipient, uint8(hTokens.length), _settlementNonce, hTokens, tokens, _amounts, _deposits, _params );
run the following code in chisel:
chisel Welcome to Chisel! Type
!help
to show available commands. ā bytes1(bytes1(0x02) & 0x0F) Type: bytes1 ā Data: 0x02 ā
Error
#0 - c4-pre-sort
2023-10-08T05:15:53Z
0xA5DF marked the issue as duplicate of #882
#1 - c4-pre-sort
2023-10-08T14:56:07Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T10:03:19Z
alcueca marked the issue as satisfactory
š Selected for report: 3docSec
Also found by: 0xStalin, 0xadrii, KingNFT, Limbooo, T1MOH, Tendency, ZdravkoHr, ciphermarco, jasonxiale, lsaudit, minhtrng, rvierdiiev, wangxx2026
39.2026 USDC - $39.20
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L936-L944 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L1204-L1217
Since BranchBridgeAgent._requiresEndpoint
and RootBridgeAgent._requiresEndpoint
check the srcAddress incorrectly, which will cause the tx will always revert on dst chain.
Since BranchBridgeAgent._requiresEndpoint
and RootBridgeAgent._requiresEndpoint
share the same root of casue, I will use BranchBridgeAgent._requiresEndpoint
as an example
In BranchBridgeAgent._requiresEndpoint, the function first get the src address by address(uint160(bytes20(_srcAddress[20:]))))
and then compares it with rootBridgeAgentAddress
in BranchBridgeAgent.sol#L943
/// @notice Internal function for caller verification. To be overwritten in `ArbitrumBranchBridgeAgent'. function _requiresEndpoint(address _endpoint, bytes calldata _srcAddress) internal view virtual { //Verify Endpoint if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); if (_endpoint != lzEndpointAddress) revert LayerZeroUnauthorizedEndpoint(); //Verify Remote Caller if (_srcAddress.length != 40) revert LayerZeroUnauthorizedCaller(); if (rootBridgeAgentAddress != address(uint160(bytes20(_srcAddress[20:])))) revert LayerZeroUnauthorizedCaller(); <------- Here }
But according to layerZero's document, and example provided by layerzero, the address should be _srcAddress[0:20]
instead of _srcAddress[20:]
Quoting from layerzero
And also the example code from layerzero
mapping(address => uint) public addrCounter; mapping(uint16 => bytes) public trustedRemoteLookup; // override from ILayerZeroReceiver.sol function lzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) override external { require(msg.sender == address(endpoint)); require(keccak256(_srcAddress) == keccak256(trustedRemoteLookup[_srcChainId]); address fromAddress; assembly { fromAddress := mload(add(_srcAddress, 20)) <----------- Here the code uses first 20 bytes } addrCounter[fromAddress] += 1; }
VIM
$git diff BranchBridgeAgent.sol diff --git a/src/BranchBridgeAgent.sol b/src/BranchBridgeAgent.sol index b076d2d..236ad35 100644 --- a/src/BranchBridgeAgent.sol +++ b/src/BranchBridgeAgent.sol @@ -940,7 +940,7 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants {
//Verify Remote Caller if (_srcAddress.length != 40) revert LayerZeroUnauthorizedCaller();
if (rootBridgeAgentAddress != address(uint160(bytes20(_srcAddress[20:])))) revert LayerZeroUnauthorizedCaller();
if (rootBridgeAgentAddress != address(uint160(bytes20(_srcAddress[0:20])))) revert LayerZeroUnauthorizedCaller();
}
/// @notice Modifier that verifies caller is Branch Bridge Agent's Router.
Other
#0 - c4-pre-sort
2023-10-13T05:50:11Z
0xA5DF marked the issue as duplicate of #439
#1 - c4-pre-sort
2023-10-13T05:50:17Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T09:42:13Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-26T09:44:58Z
alcueca marked the issue as satisfactory
#4 - alcueca
2023-10-26T09:52:41Z
There are very high effort submissions in this duplicate group. All others are getting 50% so that the few very high effort ones get double rewards.
#5 - c4-judge
2023-10-26T09:52:45Z
alcueca marked the issue as partial-50
š 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
RootBridgeAgent.getFeeEstimate
and BranchBridgeAgent.getFeeEstimate
doesn't comply with layerZero' ChecklistFile:
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L160-L173 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L140-L153
Quoting from LayerZero Integration Checklist:
Do not hardcode useZro to false when estimating fees and sending messages. Pass it as a parameter instead.
BranchBridgeAgent._createDepositMultiple doesn't check if the parameter's length is greater than zero, if the input array's length is zero, gas will wasted.
BranchPort.setCoreRouter
is never called.File: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L331-L335 Function BranchPort.setCoreRouter requires the caller to be router, but the function is never called within current codebase
RootBridgeAgent._performCall
and RootBridgeAgent._performRetrySettlementCall
Both RootBridgeAgent._performCall and RootBridgeAgent._performRetrySettlementCall can handle Root <==> ArbBrach messages without layerZero messaging, and RootBridgeAgent._performFallbackCall missing this kind of code
File:
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/VirtualAccount.sol#L51-L63
According to VirtualAccount.sol#L17, VirtualAccount.sol
is able to receive ERC1155, but the contract doesn't an external function to withdraw ERC1155 like VirtualAccount.withdrawERC20
and VirtualAccount.withdrawERC721
There are many functions in the code are defined as payable, especially for ArbitrumCoreBranchRouter.sol
, and ArbitrumBranchBridgeAgent.sol
.
Normally the functions need to be defined as payable because the function need to pay the layerzero gas fee, but for ArbitrumCoreBranchRouter.sol
, and ArbitrumBranchBridgeAgent.sol
, calls will never be cross-chain, so those function need to pay the gas fee, thus those function need not to be payable.
Take ArbitrumCoreBranchRouter.addLocalToken as an example, if users calls the the function with some ETH(because the user need to send ETH as gas fee for layerZero on other branch chains except ARB, and user doesn't know that), the ETH will stuck in the contract
function addLocalToken(address _underlyingAddress, GasParams calldata) external payable override { //Encode Data, no need to create local token since we are already in the global environment bytes memory params = abi.encode( _underlyingAddress, address(0), string.concat("Arbitrum Ulysses ", ERC20(_underlyingAddress).name()), string.concat("arb-u", ERC20(_underlyingAddress).symbol()), ERC20(_underlyingAddress).decimals() ); // Pack FuncId bytes memory payload = abi.encodePacked(bytes1(0x02), params); //Send Cross-Chain request (System Response/Request) IBridgeAgent(localBridgeAgentAddress).callOutSystem(payable(msg.sender), payload, GasParams(0, 0)); }
Within the code, localChainId
and rootChainId
those variables' type is uint16, but according to Block and Transaction Properties, block.chainid's type should be uint
block.chainid (uint): current chain id
#0 - c4-pre-sort
2023-10-15T13:01:34Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-21T05:46:13Z
alcueca marked the issue as grade-a