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: 9/175
Findings: 2
Award: $3,886.65
🌟 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
Users can create virtual accounts in the root chain to enable them managing their assets and performing calls to other contracts via payableCall
function.
But as can be noticed; this function can be accessed by anyone as it doesn't have an access modifier for authorized callers.
Given that this contract is designed to receive both native assets and tokens via the root bridge, it becomes susceptible to various attacks, potentially resulting in the loss of funds.
One possible attack scenario involves an attacker crafting a call from this contract to interact with an ERC20 contract to approve himself of these tokens, and then stealing them.
VirtualAccount.payableCall function
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; } }
Manual Testing.
Add requiresApprovedCaller
modifier to the payableCall
function:
-function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { +function payableCall(PayableCall[] calldata calls) public payable requiresApprovedCaller returns (bytes[] memory returnData) {
Access Control
#0 - c4-pre-sort
2023-10-08T14:13:16Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:53:37Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:30:11Z
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#L262-L273 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L538-L571
Users who interact with the protocol with "EOAs" (externally owned accounts) will be using the same address that is created on all evm chains for these accounts.
But users of account abstraction wallets (which are unique smart contract instances deployed on individual chains) will have different addresses on different chains.
This will introduce problems for the users who use these wallets; as the protocol assumes in all of its calls (signed calls in particular) that the user address who initiated the L0 call in the branch chain is the same address who will be receiving the result of these calls in the root chain (and later in the other chains); i.e: msg.sender
address.
The problem clearly arises when the user initiates a signed call to the root bridge with his account abstraction wallet (that will be the msg.sender
of this call), and the root bridge agent will create a virtual account for the user with the address extracted from the payload of the call (the extracted address here will be the same address of the msg.sender
of this call in the branch chain; but this address will not be owned by the user in the root chain as the account abstraction wallet will create a different address for the user in different chains):
// Get User Virtual Account VirtualAccount userAccount = IPort(localPortAddress).fetchVirtualAccount( address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED]))) );
This virtual account of the user will be receiving funds and minted global hTokens
(virtualized asset representation) in the root chain (Arbitrum).
But the created virtual account for the user will not be accessible by the same user who initiated the call in the branch chain with his abstract wallet account (different address on different chains), so he will not be able to withdraw the assets sent to the virtual account as he is not the same address who can access this contract; which will lead to loss of user assets as they become inaccessible VirtualAccount:
/// @inheritdoc IVirtualAccount function withdrawNative(uint256 _amount) external override requiresApprovedCaller { msg.sender.safeTransferETH(_amount); } /// @inheritdoc IVirtualAccount function withdrawERC20(address _token, uint256 _amount) external override requiresApprovedCaller { _token.safeTransfer(msg.sender, _amount); } /// @inheritdoc IVirtualAccount function withdrawERC721(address _token, uint256 _tokenId) external override requiresApprovedCaller { ERC721(_token).transferFrom(address(this), msg.sender, _tokenId); } //.... some code modifier requiresApprovedCaller() { if (!IRootPort(localPortAddress).isRouterApproved(this, msg.sender)) { if (msg.sender != userAddress) { revert UnauthorizedCaller(); } } _; }
An example of where the msg.sender
address is used to make calls by the branch, where it will be received by the root to create a virtual account for him:
BranchBridgeAgent.callOutSigned
function callOutSigned(address payable _refundee, bytes calldata _params, GasParams calldata _gParams) external payable override lock { //Encode Data for cross-chain call. bytes memory payload = abi.encodePacked(bytes1(0x04), msg.sender, depositNonce++, _params); //Perform Signed Call without deposit _performCall(_refundee, payload, _gParams); }
RootBridgeAgent.lzReceiveNonBlocking
// DEPOSIT FLAG: 4 (Call without Deposit + msg.sender) } else if (_payload[0] == 0x04) { // 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 4 - RootBridgeAgentExecutor(bridgeAgentExecutorAddress).executeSignedNoDeposit(address(userAccount), localRouterAddress, data, _srcChainId _execute( nonce, abi.encodeWithSelector( RootBridgeAgentExecutor.executeSignedNoDeposit.selector, address(userAccount), localRouterAddress, _payload, srcChainId ), srcChainId ); // Toggle Router Virtual Account use for tx execution IPort(localPortAddress).toggleVirtualAccountApproved(userAccount, localRouterAddress);
Manual Testing.
When users initiate a signed call from the BranchBridgeAgent
; allow users to pass the address where they would like to interact with the protocol in the root chain (used as their address in the root chain) instead of using the msg.sender
address that will be encoded in the call payload.
Other
#0 - c4-pre-sort
2023-10-13T15:33:52Z
0xA5DF marked the issue as duplicate of #877
#1 - c4-pre-sort
2023-10-13T15:34:01Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T10:15:22Z
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:33Z
alcueca marked the issue as duplicate of #351
#5 - c4-judge
2023-10-27T09:22:22Z
alcueca marked the issue as partial-50
#6 - alcueca
2023-10-27T10:49:46Z
Downgrading the whole lot to Medium again.
#7 - c4-judge
2023-10-27T10:49:55Z
alcueca changed the severity to 2 (Med Risk)
#8 - c4-judge
2023-10-27T10:56:03Z
alcueca marked the issue as satisfactory
#9 - c4-judge
2023-10-31T15:49:32Z
alcueca changed the severity to 3 (High Risk)