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: 5/175
Findings: 8
Award: $6,614.45
🌟 Selected for report: 2
🚀 Solo Findings: 2
🌟 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
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L85
VirtualAccount.payableCall can be called by anyone, which means that all funds can be stolen.
When user bridges tokens from any chain, then they are bridged to the user's virtual account. So eventually funds are inside this wallet.
VirtualAccount
is going to be permissioned, so it can be only called by owner or approved routers.
The problem is that VirtualAccount.payableCall
function doesn't use that requiresApprovedCaller
modifier. As result anyone can call function and steal everything that is inside.
VsCode
Use same requiresApprovedCaller
modifier to restrict calling.
Error
#0 - c4-pre-sort
2023-10-08T14:32:37Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:40:30Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:31:36Z
alcueca marked the issue as satisfactory
🌟 Selected for report: Tendency
Also found by: QiuhaoLi, peakbolt, rvierdiiev
787.0241 USDC - $787.02
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L940-L946
RootBridgeAgent._performFallbackCall will not work for arbitrum branch bridge
In case if user would like to receive result about failing tx on source chain, then he provides _hasFallbackToggled
param. In that case, RootBridgeAgent
will send falback message back and mark deposit as STATUS_RETRIEVE
. STATUS_RETRIEVE
means that it will be not possible to retry this deposit on root chain.
In order to send fallback _performFallbackCall
function is used. And this function will just call LZ.send to the destination.
This will mark deposit as failed on initial chain. Once it's marked as failed, then user can redeem deposit. So user will likely use fallback, when he wants to try do some actions and be able to redeem once it's failed.
ArbitrumBranchBridgeAgent and RootBridgeAgent are both on same chain. That's why they don't speak through the LZ. But in case RootBridgeAgent._performFallbackCall doesn't check, that caller is from same chain and uses LZ to send fallback. As result, function will always fail, as it will request LZ to relay to current chain. Because of that users on arbitrum branch will not have ability to use fallback functionality.
VsCode
In case if destination chain is same as local, then call arbitrum bridge agent directly.
Error
#0 - c4-pre-sort
2023-10-15T04:54:36Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-15T04:54:40Z
0xA5DF marked the issue as primary issue
#2 - 0xBugsy
2023-10-16T18:13:38Z
We answered this here: https://github.com/code-423n4/2023-09-maia-findings/issues/710
So we believe this is a duplicate
#3 - c4-judge
2023-10-26T11:14:01Z
alcueca marked the issue as duplicate of #710
#4 - c4-judge
2023-10-26T11:15:18Z
alcueca marked the issue as satisfactory
🌟 Selected for report: nobody2018
Also found by: 0xStalin, 0xnev, 3docSec, Arz, Koolex, Kow, OMEN, gumgumzum, minhtrng, perseverancesuccess, rvierdiiev, windhustler
46.9091 USDC - $46.91
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L423-L431
Airdropped amount is not refunded to the user in case if call has reverted
When user initiates deposit for example, then he provides gas params, which are used by LZ. Version 2 is used here, because it's needed to top up root bridge agent with native token, so it can initiate another LZ call back. So in case if user would like to execute some action on root chain and then bridge out(create settlement), then user will airdrop root bridge agent thorugh the LZ.
Now, let's check how LZ txs are executed on RootBridgeAgent.
RootBridgeAgent.lzReceive
will always succeed in case if it's not called from same chain(not through LZ). This is done to not allow user to replay LZ tx, once it was tried to be executed.
RootBridgeAgent.lzReceive
function calls lzReceiveNonBlocking
function. And this function calls _execute
function. Now we have 2 version of this function. One will revert in case if call didn't succeed and another will send fallback, if requested by user.
_performFallbackCall
function will send all funds to LZ, which means that user will receive refund, but all other calls, when fallback was not requested will not send refunds to the user. This funds will be left inside contract after the call.
Example. 1.User initiates deposit and also would like to do some swapping on root chain, so he would like to withdraw other token at the end. 2.User provides LZ gas params to top up root bridge agent, so it can then send settlement. So user pays for it. 3.Swap on some exchange platform has failed, because of slippage, so call reverted. 4.Airdropped amount was not refund to user and left in the root bridge agent. Someone else used it later. 5.User is able to retry deposit, but he should to provide new amount to LZ to airdrop root bridge agent again.
VsCode
Make payload
for every function have encoded refundee
address(as first param maybe) and then decode it and send all left funds to the refundee if call has failed. This can be done inside execute
function. So in case if it's call without fallback or fallback was not requested and call has failed, then refund all contract's balance.
Error
#0 - c4-pre-sort
2023-10-13T06:30:14Z
0xA5DF marked the issue as duplicate of #887
#1 - c4-pre-sort
2023-10-13T06:30:20Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T09:46:31Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2023-10-25T09:49:53Z
alcueca marked the issue as partial-50
#4 - c4-judge
2023-11-03T10:52:29Z
alcueca marked the issue as duplicate of #518
🌟 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/main/src/RootBridgeAgent.sol#L1212-L1214
Root bridge agent will not be able to communicate with branch bridge agents. User can lose funds.
Bridges communicate with each other using LZ messages. requiresEndpoint
modifier is used to check if call is authorized.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1204-L1217
modifier requiresEndpoint(address _endpoint, uint16 _srcChain, bytes calldata _srcAddress) virtual { if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); if (_endpoint != getBranchBridgeAgent[localChainId]) { if (_endpoint != lzEndpointAddress) revert LayerZeroUnauthorizedEndpoint(); if (_srcAddress.length != 40) revert LayerZeroUnauthorizedCaller(); if (getBranchBridgeAgent[_srcChain] != address(uint160(bytes20(_srcAddress[PARAMS_ADDRESS_SIZE:])))) { revert LayerZeroUnauthorizedCaller(); } } _; }
In case if _endpoint
is bridge agent from arbitrum, then no more checks are done.
Otherwise, modifier checks that its indeed LZ called function and then it checks, that caller from source chain is allowed bridge agent for that chain.
In order to get source chain caller, function does _srcAddress[PARAMS_ADDRESS_SIZE:]
, which will take last 20 bytes of _srcAddress
.
This is incorrect and in reality, function fetched destination
instead of caller, which will be address(this)
in context of modifier.
When LZ packs _srcAddress
, then first is source chain caller and 2 is destination.
When protocol imitates LZ in tests, then they pack destination first. Because of that their tests work.
I believe that the reason why protocol fetched source address in such way is this LZ example, where they use 20 bytes offset.
But the difference here is that example uses assembly and with this 20 bytes offset they just want to skip length of bytes array. But protocol uses calldata array and they don't should handle length.
This simple functions can be run in remix. Using encode
function you can get bytes array. And 2 other functions will fetch source address using LZ example and protocol example. You will see, that assembly function will return first address, while calldata version will return second address.
function testCalldata(bytes calldata path) public view returns(address ) { return address(uint160(bytes20(path[20:]))); } function testMemory(bytes memory path) public view returns(address result) { assembly { result := mload(add(path, 20)) } } function encode() public view returns(bytes memory result) { address a = address(1); address b = address(2); bytes memory scrBytes = abi.encodePacked(a); bytes memory path = abi.encodePacked(scrBytes, b); return path; }
Impact of this is severe. Actually protocol will not work. All communication between root bridge and branch bridges will not work, except Arbitrum branch.
Also, it's possible that new chain will be added with already deployed tokens on it. So user who will bridge those tokens, will lose deposit, as message will not be executable on root bridge agent.
VsCode
In order to get source address use _srcAddress[:PARAMS_ADDRESS_SIZE]
Error
#0 - c4-pre-sort
2023-10-13T06:30:46Z
0xA5DF marked the issue as primary issue
#1 - c4-pre-sort
2023-10-13T06:30:56Z
0xA5DF marked the issue as duplicate of #439
#2 - c4-pre-sort
2023-10-13T06:31:01Z
0xA5DF marked the issue as sufficient quality report
#3 - c4-judge
2023-10-26T09:42:13Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-10-26T09:48:56Z
alcueca marked the issue as satisfactory
#5 - alcueca
2023-10-26T09:51:24Z
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.
#6 - c4-judge
2023-10-26T09:51:34Z
alcueca marked the issue as partial-50
🌟 Selected for report: rvierdiiev
5613.8892 USDC - $5,613.89
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol#L168-L170 https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L140-L146
ArbitrumCoreBranchRouter.executeNoSettlement can't handle setCoreBranchRouter
function.
ArbitrumCoreBranchRouter extends CoreBranchRouter and overrides its executeNoSettlement
function.
CoreBranchRouter.executeNoSettlement can handle 0x07 function, but ArbitrumCoreBranchRouter.executeNoSettlement
can't.
I believe this is because, previously CoreBranchRouter.executeNoSettlement also didn't handle 0x07 function and when support for it was added, then ArbitrumCoreBranchRouter was not updated with new function.
VsCode
Add support of 0x07 function.
Error
#0 - c4-pre-sort
2023-10-14T10:48:19Z
0xA5DF marked the issue as sufficient quality report
#1 - 0xA5DF
2023-10-14T10:48:30Z
Might be intended functionality, leaving open for sponsor to comment
#2 - c4-pre-sort
2023-10-14T10:48:35Z
0xA5DF marked the issue as primary issue
#3 - c4-sponsor
2023-10-16T18:14:09Z
0xBugsy (sponsor) confirmed
#4 - c4-judge
2023-10-25T12:34:55Z
alcueca marked the issue as satisfactory
#5 - c4-judge
2023-10-27T10:28:08Z
alcueca marked the issue as selected for report
#6 - 0xBugsy
2023-11-12T18:04:08Z
🌟 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
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L509-L518
RootPort.setCoreBranchRouter doesn't have enough validation. As result new bridge can be set, that will have access to the funds, but will not be able to communicate with RootBridgeAgent.
When new bridge is added to the chain, then this bridge is marked as known inside RootBridgeAgent. As result, only 1 known bridge per chain can call messages to the RootBridgeAgent.
RootPort.setCoreBranchRouter
can be called by owner in order to add router and bridge to the branch chain.
This function does nothing on Root side, but adds router and bridge on branch side. As result new router is added and new bridge is added. And this bridge now has access to the port funds, which means that users can start brigding with it.
The problem is that in case if this bridge was not inside RootBridgeAgent.getBranchBridgeAgent[chain], then messages from this bridge will not be accepted, so they will just revert and noone will be able to replay them.
While this function is fully controlled by owner, incorrect usage of it will make big problems, so it's better to add additional check.
VsCode
I believe that RootPort.setCoreBranchRouter
function should check that provided bridge is inside RootBridgeAgent.getBranchBridgeAgent[chain]. Only then it should allow to proceed.
Error
#0 - c4-pre-sort
2023-10-14T11:21:37Z
0xA5DF marked the issue as low quality report
#1 - 0xA5DF
2023-10-14T11:21:43Z
Covered by bot report
#2 - alcueca
2023-10-23T14:51:09Z
I don't think this was covered in the bot report. QA.
#3 - c4-judge
2023-10-23T14:51:15Z
alcueca changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-10-23T14:51:52Z
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-
101.6449 USDC - $101.64
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L681-L695 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L938-L948
User always overpay for fallback call
In case if user would like to receive fallback call for it deposit, then he can provide _hasFallbackToggled
param as true. This means that in case if this deposit will fail in root bridge agent, then fallback will be called to the original chain.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L938-L948
function _performFallbackCall(address payable _refundee, uint32 _depositNonce, uint16 _dstChainId) internal { //Sends message to LayerZero messaging layer ILayerZeroEndpoint(lzEndpointAddress).send{value: address(this).balance}( _dstChainId, getBranchBridgeAgentPath[_dstChainId], abi.encodePacked(bytes1(0x04), _depositNonce), payable(_refundee), address(0), "" ); }
As you can see _adapterParams
of layer zero send
function is provided as empty, which means that default gas amount will be used for such call. Currently it's 200_000 gas.
https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters
Abstract: Every transaction costs a certain amount of gas. Since LayerZero delivers the destination transaction when a message is sent it must pay for that destination gas. A default of 200,000 gas is priced into the call for simplicity.
But if you will look how fallback is handled on BranchBridgeAgent
then you will see, that it will never use such big amount of gas.
Put this into BranchBridgeAgentTest
.
function testFallbackGasAmount() public { // Encode Fallback message bytes memory fallbackData = abi.encodePacked(bytes1(0x04), uint32(1)); // Call 'Fallback' vm.prank(lzEndpointAddress); uint256 gasStart = gasleft(); bAgent.lzReceive(rootChainId, abi.encodePacked(bAgent, rootBridgeAgentAddress), 1, fallbackData); console2.log("gas used: ", gasStart - gasleft()); } [PASS] testFallbackGasAmount() (gas: 44011) Logs: gas used: 35288
As you can see user overpays more than 150_000 of gas, which can be not cheap as branch can be ethereum.
VsCode
You can calculate approx gas amount to call fallback(for example 50_000) and set it as variable in the root brodge agent that can be changed(so each fallback call will use that amount of gas). Or you can allow users to provide gas amount for fallback, which is worse as they will also need to pay for this info to be relayed to root chain.
Error
#0 - c4-pre-sort
2023-10-13T15:55:22Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-13T15:55:26Z
0xA5DF marked the issue as primary issue
#2 - 0xA5DF
2023-10-13T16:44:01Z
Notice #383 talks about the same issue, but claims the amount might change (causing the call to revert):
However, in the future versions of either blockchain or LayerZero protocol, the required execution cost and default provided gas value might change
#3 - c4-sponsor
2023-10-16T18:08:40Z
0xBugsy marked the issue as disagree with severity
#4 - c4-sponsor
2023-10-16T18:08:45Z
0xBugsy (sponsor) confirmed
#5 - 0xBugsy
2023-10-16T18:10:44Z
Although this is true, using non-default adapterParams also comes with its own costs attached but that being said it is something that should be worth double checking on our behalf.
#6 - alcueca
2023-10-25T12:37:34Z
This is a (sizable) gas report.
#7 - c4-judge
2023-10-25T12:38:00Z
alcueca changed the severity to G (Gas Optimization)
#8 - c4-judge
2023-10-25T12:38:16Z
alcueca marked the issue as grade-a
#9 - c4-judge
2023-10-25T12:38:22Z
alcueca marked the issue as selected for report
#10 - c4-judge
2023-10-27T10:11:24Z
alcueca marked issue #383 as primary and marked this issue as a duplicate of 383
#11 - c4-judge
2023-10-28T06:02:35Z
alcueca marked the issue as selected for report
#12 - rvierdiyev
2023-10-31T09:38:18Z
hello @alcueca i would like to know why do you think it's gas report? the main idea of this report is that users are losing their funds that they pay for LZ gas and they don't have ability to chnage that. and this has nothing common with c4 gas reports, the purpose of which is to rewrite some parts of code to decreases gas usage, according to gas saving patterns in evm
my point of view is that it's a loss of funds for users(like when protocol overcharges fees by mistake), as they constantly pay more gas which means that they are overcharged by protocol. as you texted in the disscussions page, you judge loss of gas as medium, that's why it think that this report is more medium than gas.
the last parallel that i would like to show is to think about LZ gas as fee for protocol. in case if fee is overpaid and more amount is burnt than needed(for example user can provide fee limit to not have bigger loss, but function doesn't have such ability), then this is always judged as medium. so here it's same case: user overpays fee, because simply doesn't have ability to set appropriate limit. hope i translated it clear. thanks
#13 - QiuhaoLi
2023-10-31T13:02:24Z
you judge loss of gas as medium
FYI: https://github.com/code-423n4/2023-09-maia-findings/discussions/906#discussioncomment-7435032 I think the stuck airdrop token is not the same as using the default gas (gasAmount
instead of nativeForDst
in the Relayer Adapter Parameters ) more than needed situation here.
#14 - alcueca
2023-11-03T12:38:38Z
I understand that the decision might be a bit unconventional, but I see this report as a gas report. The gas cost of an operation is X, but if you do this it will be Y, with Y < X.
It is not like gas tokens getting stuck. Users get a gas estimation for their transaction, and submit some extra in case something happens, but hoping to get it back. If they never get it back, they do feel that they have been shortchanged.
In this case, users will be told beforehand that their gas use will be X if they go the fallback route. They will never know that it could have been Y.
However, given that this finding provides a saving of more than 150k gas on some cases, which is vastly more than other gas reports, it was selected as best.