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: 35/175
Findings: 4
Award: $178.73
🌟 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
The unrestricted access to the payableCall function can result in unauthorized transactions, leading to the loss of funds in the Virtual Account. Additionally, since the Virtual Account is designed to be integrated with various dApps, this vulnerability could also lead to the loss of assets in those dApps.
The payableCall
function in the VirtualAccount
contract allows for multiple calls to be made to other contracts. The function is publicly accessible and does not have any restrictions on who can call it.
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(); }
The VirtualAccount as a Omnichain Wallet, it stores assets of users. A malicious user can call the payableCall
function to drain the funds of the VirtualAccount.
The VirtualAccount is used to intergated in different dAPP. It means that anyone can interact with the dApp as the VirtualAccount and drain the funds related to the Virtual Account.
Manual
Add requiresApprovedCaller
modifier to restrict the caller of 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:03:49Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:37:12Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:29:11Z
alcueca marked the issue as satisfactory
🌟 Selected for report: LokiThe5th
Also found by: 0xadrii, 33BYTEZZZ, 3docSec, Bauchibred, DevABDee, Koolex, Kow, Limbooo, QiuhaoLi, Tendency, ast3ros, ihtishamsudo, kodyvim, lsaudit, neumo, peakbolt, windhustler
40.0102 USDC - $40.01
https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L578-L584 https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L720 https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L748-L751
If a transaction fails on the destination chain, the channel between the source and destination chains becomes blocked. This effectively halts the operation of the protocol until the issue is resolved or a force resume message is delivered by the dstUA contract.
If a transaction fails in the destination chain, the channel between the source chain and destination chain will be blocked. The source chain cannot send any message to the destination chain until the transaction is success or force resume message delivery, only by the dstUA contract.
https://layerzero.gitbook.io/docs/faq/messaging-properties#message-ordering
LayerZero provides ordered delivery of messages from a given sender to a destination chain, i.e. srcUA-> dstChain. In other words, the message order nonce is shared by all dstUA on the same dstChain. That's why a STORED message blocks the message pathway from srcUA to all dstUA on the same destination chain. If it isn't necessary to preserve the sequential nonce property for a particular dstUA the sender must add the nonce into the payload and handle it end-to-end within the UA. UAs can implement a non-blocking pattern in their contract code.
LayerZero recommends implementing a non-blocking pattern where all errors and exceptions are caught locally for future retries. This ensures that the channel remains open even if a transaction fails.
(bool success, bytes memory reason) = address(this).excessivelySafeCall( gasleft(), 150, abi.encodeWithSelector(this.nonblockingLzReceive.selector, _srcChainId, _srcAddress, _nonce, _payload) ); // try-catch all errors/exceptions if (!success) { _storeFailedMessage(_srcChainId, _srcAddress, _nonce, _payload, reason); }
However, in Maia protocol, we can see that not all error, revert is caught
function lzReceive(uint16, bytes calldata _srcAddress, uint64, bytes calldata _payload) public override { address(this).excessivelySafeCall( gasleft(), 150, abi.encodeWithSelector(this.lzReceiveNonBlocking.selector, msg.sender, _srcAddress, _payload) ); }
In contrast, Maia's current implementation does not catch all errors or reverts. For example, the _execute
function can be reverted, but this is not caught.
if (!success) revert ExecutionFailure();
} else { // If no fallback is requested revert allowing for settlement retry. revert ExecutionFailure(); }
Manual
Catch all the revert in _execute functions and save for future retry like in code example provided by LayerZero.
Error
#0 - c4-pre-sort
2023-10-11T12:34:43Z
0xA5DF marked the issue as duplicate of #875
#1 - c4-pre-sort
2023-10-11T12:34:47Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-22T04:53:27Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2023-10-22T04:56:34Z
alcueca marked the issue as duplicate of #399
#4 - c4-judge
2023-10-22T04:59:48Z
alcueca marked the issue as partial-50
#5 - alcueca
2023-10-22T05:00:30Z
50% for lack of PoC, no mention of forceResumeReceive
, and general depth in comparison with other satisfactory-ranked reports.
#6 - c4-judge
2023-10-22T05:12:00Z
alcueca marked the issue as satisfactory
🌟 Selected for report: kodyvim
Also found by: 0xnev, Kow, QiuhaoLi, SpicyMeatball, ast3ros, ayden, bin2chen, chaduke, jasonxiale, minhtrng, nobody2018
112.9294 USDC - $112.93
https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/RootBridgeAgent.sol#L1090 https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L638-L660 https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L651
The incorrect flag setting prevents the fallback mechanism from executing in the destination chain. As a result, users cannot retrieve their tokens, affecting the reliability of the protocol.
When settling multiple assets and perform a remote call to a branch chain, the function RootBridgeAgent._createSettlementMultiple
sets the fallback flag based on the _hasFallbackToggled
variable. The flag is set to either bytes1(0x02) & 0x0F
or bytes1(0x02)
.
_hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02)
The logic error is both flag settings evaluate to 0x02
:
0x02 & 0x0F = 0x02 (0000 0010 & 0000 1111 = 0000 0010)
This means that the fallback flag is always set to 0x02
, regardless of whether _hasFallbackToggled
is true or false.
The BranchBridgeAgent contract checks for a flag value of _payload[0] == 0x82
to trigger the fallback mechanism. Because the flag is incorrectly set to 0x02
, the fallback mechanism is never triggered, even when it should be.
// DEPOSIT FLAG: 2 (Multiple Settlement) } else if (flag == 0x02) { // Parse recipient address payable recipient = payable(address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED])))); // Parse deposit nonce nonce = uint32(bytes4(_payload[22:26])); //Check if tx has already been executed if (executionState[nonce] != STATUS_READY) revert AlreadyExecutedTransaction(); //Try to execute remote request // Flag 2 - BranchBridgeAgentExecutor(bridgeAgentExecutorAddress).executeWithSettlementMultiple(recipient, localRouterAddress, _payload) _execute( _payload[0] == 0x82, nonce, recipient, abi.encodeWithSelector( BranchBridgeAgentExecutor.executeWithSettlementMultiple.selector, recipient, localRouterAddress, _payload ) );
Manual
Correct the fallback flag setting in the _createSettlementMultiple
function.
- _hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02) + _hasFallbackToggled ? bytes1(0x82) : bytes1(0x02)
Error
#0 - c4-pre-sort
2023-10-08T05:14:12Z
0xA5DF marked the issue as duplicate of #882
#1 - c4-pre-sort
2023-10-08T14:49:28Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T10:01:12Z
alcueca marked the issue as satisfactory
🌟 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 the protocol the depositNonce
and settlementNonce
in bridgeAgent are set to uint32
. The maximum of those numbers are only 2^32 ~ 4 bil. It is too low for a protocol that is going to have a lot of bridge transactions. The transactions are going to be revert when the nonces are overflow.
https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/BranchBridgeAgent.sol#L84 https://github.com/code-423n4/2023-09-maia/blob/ffbe532c6f5224a55ce099b4016bd8806bdbc913/src/RootBridgeAgent.sol#L75
Normally, the nonce should be set to uint64
which is safe.
When branch bridge agent is disable, it cannot be enable again, the only option is creating a new one.
// Check if BridgeAgentFactory is active if (IPort(_localPortAddress).isBridgeAgentFactory(_newBridgeAgentFactoryAddress)) { // If so, disable it. IPort(_localPortAddress).toggleBridgeAgentFactory(_newBridgeAgentFactoryAddress); } else { // If not, add it. IPort(_localPortAddress).addBridgeAgentFactory(_newBridgeAgentFactoryAddress); }
setCoreRouter
in BranchPort cannot be calledThe function setCoreRouter can only be called by coreRouter. However the contract CoreBranchRouter has no function to call this function.
function setCoreRouter(address _newCoreRouter) external override requiresCoreRouter {
The natspec below is for a different setter function, not the bridgeOut function.
/** * @notice Setter function to decrease local hToken supply. * @param _localAddress token address. * @param _amount amount of tokens. * @param _deposit amount of underlying tokens. * */ function bridgeOut( address _depositor, address _localAddress, address _underlyingAddress, uint256 _amount, uint256 _deposit ) external;
#0 - c4-pre-sort
2023-10-14T08:59:17Z
0xA5DF marked the issue as sufficient quality report
#1 - 0xA5DF
2023-10-14T08:59:36Z
L1 is dupe of #834 TODO
#2 - c4-judge
2023-10-20T13:40:05Z
alcueca marked the issue as grade-b