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: 72/175
Findings: 2
Award: $39.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
payableCall()
misses modifier requiresApprovedCaller
. As a result anyone can perform any call from VirtualAccount. For example it can be ERC20/ERC721 transfer.
I really don't see need to prove such an obvious issue with executable test.
Anyone can specify arbitrary target and calldata and perform call from VirtualAccount:
function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { uint256 valAccumulator; uint256 length = calls.length; returnData = new bytes[](length); PayableCall calldata _call; for (uint256 i = 0; i < length;) { _call = calls[i]; uint256 val = _call.value; // Humanity will be a Type V Kardashev Civilization before this overflows - andreas // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 unchecked { valAccumulator += val; } bool success; @> if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData); if (!success) revert CallFailed(); unchecked { ++i; } } // Finally, make sure the msg.value = SUM(call[0...i].value) if (msg.value != valAccumulator) revert CallFailed(); }
Manual Review
Add modifier requiresApprovedCaller
Invalid Validation
#0 - c4-pre-sort
2023-10-08T14:33:55Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:41:01Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:32:35Z
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
LayerZero passes argument bytes calldata _srcAddress
. According to documentation it contains 40 bytes, actually it is 2 addresses concatenated. You can check detail explanation in according documentation
It encodes remoteAddress + localAddress
. Where remoteAddress
is caller on source chain, and localAddress
is contract on destination chain - in other words address(this).
Problem is that it decodes localAddress
and compares to trusted caller instead of remoteAddress
.
As a result RootBridgeAgent can't receive messages from other chains
Firstly docs explain in clear manner that 20-40 bytes is address(this)
However I want to provide Tenderly debug to prove this. Take a look on Endpoint's retryPayload()
on Tenderly
Function Endpoint.retryPayload()
is fallback used to execute cross-chain message. Submitted _srcAddress = 0x3055913c90fcc1a6ce9a358911721eeb942013a1152649ea73beab28c5b49b26eb48f7ead6d4c898
- it means:
In this tx Endpoint calls ERC20 token CakeOFT to mint tokens. Address of this token is indeed this localAddress, check this url. It proves point that sender of message is encoded in first 20 bytes of _srcAddress
However you can see that RootBridgeAgent tries to read the last 20 bytes:
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(); } } _; }
Manual Review
Refactor check:
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:])))) { + if (getBranchBridgeAgent[_srcChain] != address(uint160(bytes20(_srcAddress[0:PARAMS_ADDRESS_SIZE])))) { revert LayerZeroUnauthorizedCaller(); } } _; }
Invalid Validation
#0 - c4-pre-sort
2023-10-13T06:29:35Z
0xA5DF marked the issue as duplicate of #439
#1 - c4-pre-sort
2023-10-13T06:29:40Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T09:42:12Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-26T09:48:32Z
alcueca marked the issue as satisfactory
#4 - alcueca
2023-10-26T09:51:37Z
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:51:43Z
alcueca marked the issue as partial-50