xanoutas/solidity-security-findings huggingface dataset:
`VirtualAccount.sol` CONTRACT DOES NOT CORRECTLY IMPLEMENT THE `eip1155` STANDARD WHILE INHERITING THE `ERC1155Receiver`
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The `VirtualAccount.sol` contract inherits the `ERC1155Receiver` contract to receive the `ERC1155` tokens. Both `onERC1155Received` and `onERC1155BatchReceived` functions are declared in the `VirtualAccount` contract as per the `eip1155` standard. 

The respective `ERC1155` function implementation is shown below:

    /// @inheritdoc IERC1155Receiver
    function onERC1155Received(address, address, uint256, uint256, bytes calldata)
        external
        pure
        override
        returns (bytes4)
    {
        return this.onERC1155Received.selector;
    }

    /// @inheritdoc IERC1155Receiver
    function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
        external
        pure
        override
        returns (bytes4)
    {
        return this.onERC1155BatchReceived.selector;
    }

But the issue is `VirtualAccount` does not correctly implement the `eip1155` standard when inheriting from `ERC1155TokenReceiver.sol` interface. This is because the `eip1155` standard states that `Smart contracts MUST implement the ERC-165 supportsInterface function and signify support for the ERC1155TokenReceiver interface to accept transfers`. But the `ERC-165` `supportsInterface` function is not implemented in the `VirtualAccount` contract. Hence the `VirtualAccount` contract does not comply with the `eip1155` standard to receive the ERC1155 tokens safely and correctly.

External contracts or systems might check if the `VirtualAccount` contract implements a specific interface (e.g., using ERC165) before interacting with the `VirtualAccount` contract. Since `ERC-165` `supportsInterface` function is not implemented in the `VirtualAccount` contract, the external contracts might deem `VirtualAccount` contract incompatible, and could revert the ERC1155 token transfers as a result. Thus the `VirtualAccount` contract will not function as intended.

## Proof of Concept

```solidity
contract VirtualAccount is IVirtualAccount, ERC1155Receiver {
```

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

```solidity
    /// @inheritdoc IERC1155Receiver
    function onERC1155Received(address, address, uint256, uint256, bytes calldata)
        external
        pure
        override
        returns (bytes4)
    {
        return this.onERC1155Received.selector;
    }

    /// @inheritdoc IERC1155Receiver
    function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
        external
        pure
        override
        returns (bytes4)
    {
        return this.onERC1155BatchReceived.selector;
    }
```

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

## Tools Used
Manual Review and VSCode

##
Airdropped Gas will remain in the Agent in case of failure
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L579-L583


# Vulnerability details

## Impact
The protocol uses LayerZeros [Airdrop](https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters#airdrop) mechanism to send gas to BridgeAgents which they need to pay for subsequential cross-chain-messages. If the transaction on the receiver fails, this airdropped gas will remain in the BridgeAgent and can be used up by the next caller.

## Proof of Concept

The use of the Airdrop mechanism can be seen for example in `BranchBridgeAgent._performCall`:

```js
    function _performCall(address payable _refundee, bytes memory _payload, GasParams calldata _gParams)
        internal
        virtual
    {
        ILayerZeroEndpoint(lzEndpointAddress).send{value: msg.value}(
            ...
            abi.encodePacked(uint16(2), _gParams.gasLimit, _gParams.remoteBranchExecutionGas, rootBridgeAgentAddress)
        );
    }
```

An example that shows an attempt of using the airdropped gas to perform a follow up call is in `BranchBridgeAgent._performFallBackCall`:

```js
ILayerZeroEndpoint(lzEndpointAddress).send{value: address(this).balance}(...
```

This call happens after the execution of another call has failed, for example `_executeWithSettlement`:

```js
function executeWithSettlement(address _recipient, address _router, bytes calldata _payload)
	external
	payable
	onlyOwner
{
	...
	} else {
		_recipient.safeTransferETH(address(this).balance);
	}
}
```

As can be seen, the function attempts to send back the remaining native gas to the recipient. If the execution fails, the fallback is triggered. However the fallback too can fail, if the gas balance of the bridge agent is not enough to cover the relayer costs (looking at [LayerZero code](https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/UltraLightNodeV2.sol#L150) -> `UltraLightNodeV2.send` gets called by the Endpoint). In this case, the airdropped gas is not send anywhere and can be used up by the next caller.

## Tools Used

Manual Review

## Recommended Mitigation Steps

At `lzReceive` (which can never fail due to a low level call, with additional safety), the contract balance could be sent to a refundee or recipient in case of failure:

```js
    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)
        );

        //check if !success and transfer address(this).balance to the recipient (has to be determined from payload)
    }
```


## Assessed type

Other
LayerZero endpoint can get blocked by a malicious user (or even a honest one)
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description
Contract `Endpoint`, from LayerZero is the one responsible of sending/receiving messages to/from other chains. Specifically it has function `receivePayload`, which is called by contract `UltraLightNodeV2` (the current default library of the protocol) after validating the transaction proof.
```solidity
function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress, uint64 _nonce, uint _gasLimit, bytes calldata _payload) external override receiveNonReentrant {
	// assert and increment the nonce. no message shuffling
	require(_nonce == ++inboundNonce[_srcChainId][_srcAddress], "LayerZero: wrong nonce");

	LibraryConfig storage uaConfig = uaConfigLookup[_dstAddress];

	// authentication to prevent cross-version message validation
	// protects against a malicious library from passing arbitrary data
	if (uaConfig.receiveVersion == DEFAULT_VERSION) {
		require(defaultReceiveLibraryAddress == msg.sender, "LayerZero: invalid default library");
	} else {
		require(uaConfig.receiveLibraryAddress == msg.sender, "LayerZero: invalid library");
	}

	// block if any message blocking
	StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress];
	require(sp.payloadHash == bytes32(0), "LayerZero: in message blocking");

	try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) {
		// success, do nothing, end of the message delivery
	} catch (bytes memory reason) {
		// revert nonce if any uncaught errors/exceptions if the ua chooses the blocking mode
		storedPayload[_srcChainId][_srcAddress] = StoredPayload(uint64(_payload.length), _dstAddress, keccak256(_payload));
		emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _nonce, _payload, reason);
	}
}
```

This function is the one that is responsible of executing the `lzReceive` function in the destination user application (UA in LayerZero terminology).

The last lines of the function is where we have to put our eyes to understand the issue reported here.

First, it looks for a `StoredPayload` value in a mapping, for the source chain and source address of the message:
```solidity
// block if any message blocking
StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress];
```
Then, it checks that the payload hash stored is empty, **otherwise it reverts and the message cannot be delivered**.

```solidity
require(sp.payloadHash == bytes32(0), "LayerZero: in message blocking");
```

Lastly, it tries to execute the function `lzReceive` in the destination address.
```solidity
try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) {
	// success, do nothing, end of the message delivery
} catch (bytes memory reason) {
	// revert nonce if any uncaught errors/exceptions if the ua chooses the blocking mode
	storedPayload[_srcChainId][_sr
Incorrect functionID will not trigger fallback
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When encoding a payload for settlement of multiple tokens, the fallback flag is not set when it should be. This will cause no fallback to be triggered even though the user has paid enough to cover the additional costs that are required.

## Proof of Concept
In `RootBridgeAgent._createSettlementMultiple` the function ID which contains the fallback flag as first bit is encoded like this:

```js
_hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02),
```

This bitwise AND of 0x02 with 0x0F will yield 0x02 (same as the no fallback case). Hence no fallback will be triggered in case of failure on the branch chain:

```js
function lzReceiveNonBlocking(address _endpoint, bytes calldata _srcAddress, bytes calldata _payload)
    public
    override
    requiresEndpoint(_endpoint, _srcAddress)
{
    //Save Action Flag
    bytes1 flag = _payload[0] & 0x7F; //remove leftmost bit
    ...
    } else if (flag == 0x02) {
    ...
    _execute(
        _payload[0] == 0x82, //hasFallback
    ...
```

Minimal PoC with foundry to showcase `0x02 & 0x0F == 0x02`:

```js
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

contract BitOperationsTest is Test {

    function testBitwiseAnd() public{
        assert(bytes1(0x02) & 0x0F == 0x02);
    }
}
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Looking at how `RootBridgeAgent._createSettlement` determines the first byte:

```js
_hasFallbackToggled ? bytes1(0x81) : bytes1(0x01),
```

This should be done analoguously for `_createSettlementMultiple`

```js
_hasFallbackToggled ? bytes1(0x82) : bytes1(0x02),
```


## Assessed type

Other
The governance will fail to add an ecosystem token if someone creates a hToken that uses that ecosystem token
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Ecosystem tokens are tokens that dont have an underlying token address in any branch and only the global representation exists. The governance adds them by calling `addEcosystemToken()` where the `_ecoTokenGlobalAddress` will be the Maia or Hermes token as the sponsor mentioned or any other tokens that could be added in the future.  

The problem is that anyone can create a hToken where the underlying asset is the ecosystem token and then this mapping will get updated in `setAddresses()`:

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootPort.sol#L252
```solidity
252:  getLocalTokenFromUnderlying[_underlyingAddress][_srcChainId] = _localAddress;
```

The `_underlyingAddress` equals to the `_ecoTokenGlobalAddress` and when the admin calls `addEcosystemToken()` then it will revert because of this check that is incorrect:

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

```solidity

493:   if (getLocalTokenFromUnderlying[_ecoTokenGlobalAddress][localChainId] != address(0)) {
494:       revert AlreadyAddedEcosystemToken();
495:   }
```

`getLocalTokenFromUnderlying[_ecoTokenGlobalAddress]` wont be a zero address because it was set when the attacker added the hToken. 

This check here is redundant and even if someone creates a hToken where the underlying asset will be the ecosystem token then it will not be tied to the ecosystem token in any way because it has a different global address and a different local address. 

## Impact
The governance will fail to add the ecosystem tokens and they will not work with this part of Ulysses because an attacker can easily create a hToken before the ecosystem token is set. 

## Proof of Concept

Add this to `RootTest.t.sol`

```solidity
function testAddEcosystemTokenAttack() public {
        //arbitrumMockToken will be the Maia or Hermes token
        hevm.deal(address(this), 1 ether);

        //Attacker adds the Maia or Hermes token
        arbitrumCoreRouter.addLocalToken{value: 0.0005 ether}(
            address(arbitrumMockToken), GasParams(0.5 ether, 0.5 ether)
        );

        newArbitrumAssetGlobalAddress =
            RootPort(rootPort).getLocalTokenFromUnderlying(address(arbitrumMockToken), rootChainId);

        require(
            RootPort(rootPort).getGlobalTokenFromLocal(address(newArbitrumAssetGlobalAddress), rootChainId)
                == address(newArbitrumAssetGlobalAddress),
            "Token should be added"
        );
        require(
            RootPort(rootPort).getLocalTokenFromGlobal(newArbitrumAssetGlobalAddress, rootChainId)
                == address(newArbitrumAssetGlobalAddress),
            "Token should be added"
        );
        require(
            RootPort(rootPort).getUnderlyingTokenFromLocal(address(newArbit
Incorrect srcAddress check renders all layerzero messages unusable
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L943
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L1212


# Vulnerability details

## Impact

The source address of LayerZero messages is validated on a wrong part of the calldata, which will cause all cross-chain-messages to fail on a live deployment.

## Proof of Concept

The receivers of cross-chain-messages `BranchBridgeAgent` and `RootBridgeAgent` both perform validation of the sender in their respective `requiresEndpoint` modifiers:

```js
// RootBridgeAgent
if (getBranchBridgeAgent[_srcChain] != address(uint160(bytes20(_srcAddress[PARAMS_ADDRESS_SIZE:])))) {
    revert LayerZeroUnauthorizedCaller();
}

//BranchBridgeAgent
if (rootBridgeAgentAddress != address(uint160(bytes20(_srcAddress[20:])))) revert LayerZeroUnauthorizedCaller();
```

Both try to extract the sender address from the last 20 bytes of the `_srcAddress` (which consists of 2 EVM addresses, so 40 bytes in total). The issue is that the address of the sender on the source chain is contained in the first 20 bytes not the last.

During development the assumption was made that whatever path gets send, will also be the path when receiving. Looking at `BranchBridgeAgent._performCall`, the following parameter is passed:

```js
ILayerZeroEndpoint(lzEndpointAddress).send{value: msg.value}(
    ...
    rootBridgeAgentPath,
    ...
);
```

Here, `rootBridgeAgentPath` is the 40 bytes combination of remoteAddress and localAddress mentioned above:

```js
rootBridgeAgentPath = abi.encodePacked(_rootBridgeAgentAddress, address(this));
```

It is easy to think that since `address(this)` occupies the last 20 bytes here, that the path given to the receiver will also be in the same order. 

However, LayerZero actually swaps the addresses as the path is always semantically meant to be comprised of remoteAddress first and then localAddress, relative to the current chain (see docs: [Sending on L0](https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message) and [Trusted remotes on L0](https://layerzero.gitbook.io/docs/evm-guides/master/set-trusted-remotes) + [Github example](https://github.com/LayerZero-Labs/set-trusted-remotes))

Proving on a real on-chain transaction that this is what happens:

[cross-chain tx on layerzeroscan](https://layerzeroscan.com/110/address/0x352d8275aae3e0c2404d9f68f6cee084b5beb3dd/message/106/address/0x9d1b1669c73b033dfe47ae5a0164ab96df25b944/nonce/1306956)

This link shows a cross-chain message from Arbitrum to Avalanche. Looking at the decoded transaction traces (using Tenderly, links below), we can see that the path used in lzSend is `0x9d1b1669c73b033dfe47ae5a0164ab96df25b944352d8275aae3e0c2404d9f68f6cee084b5beb3dd` while the path received in lzReceive is `0x352d8275aae3e0c2404d9f68f6cee084b5beb3dd9d1b1669c73b033dfe47ae5a0164ab96df25b944` (first and last 20
[M-16] Reentrancy in the BaseBranchRouter contract
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L88-L101
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L104-L116


# Vulnerability details

## Impact
In a Re-entrancy attack, a malicious contract calls back into the calling contract before the first invocation of the function is finished. 
This may cause the different invocations of the function to interact in undesirable ways, especially in cases where the function is updating state variables after the external calls.
This may lead to loss of funds, improper value updates, token loss, etc.
## Proof of Concept
**The callOutAndBridge function is vulnerable to reentrancy**
```sol
// Line 88-101
    function callOutAndBridge(bytes calldata _params, DepositInput calldata _dParams, GasParams calldata _gParams)
        external
        payable
        override
        lock
    {
        //Transfer tokens to this contract.
        _transferAndApproveToken(_dParams.hToken, _dParams.token, _dParams.amount, _dParams.deposit);


        //Perform call to bridge agent.
        IBridgeAgent(localBridgeAgentAddress).callOutAndBridge{value: msg.value}(
            payable(msg.sender), _params, _dParams, _gParams
        );
    }
```
**The callOutAndBridgeMultiple function is vulnerable to reentrancy**
```sol
// 104-116
    function callOutAndBridgeMultiple(
        bytes calldata _params,
        DepositMultipleInput calldata _dParams,
        GasParams calldata _gParams
    ) external payable override lock {
        //Transfer tokens to this contract.
        _transferAndApproveMultipleTokens(_dParams.hTokens, _dParams.tokens, _dParams.amounts, _dParams.deposits);


        //Perform call to bridge agent.
        IBridgeAgent(localBridgeAgentAddress).callOutAndBridgeMultiple{value: msg.value}(
            payable(msg.sender), _params, _dParams, _gParams
        );
    }
```
## Tools Used
VS Code.
## Recommended Mitigation Steps
It is recommended to add a [Re-entrancy Guard] to the functions making external calls. 
The functions should use a Checks-Effects-Interactions pattern. 
The external calls should be executed at the end of the function and all the state-changing must happen before the call.


## Assessed type

Reentrancy
Lack of force resume support for LZ which is crucially important to have
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/c0dc3550e0754571b82d7bfd8f0282ac8fa5e42f/src/RootBridgeAgent.sol#L32
https://github.com/code-423n4/2023-09-maia/blob/c0dc3550e0754571b82d7bfd8f0282ac8fa5e42f/src/BranchBridgeAgent.sol#L45


# Vulnerability details

## Impact
The User Application (LZReceiver) should implement the ILayerZeroUserApplicationConfig interface which includes the **forceResumeReceive** function. This is very important as in the worst case, it can allow the owner to unblock the queue of messages if something unexpected/unpredicted occurs.

This is crucially important to have especially in emergency situations. I've reported 2 issues related to blocking messaging channel of LZ. This explains why it is highly recommended by LayerZero. 
Please check https://layerzero.gitbook.io/docs/evm-guides/best-practice


## Proof of Concept

Please check RootBridgeAgent and BranchBridgeAgent. There is no Implementation of ILayerZeroUserApplicationConfig.



## Tools Used
Manual analysis

## Recommended Mitigation Steps

Implement **forceResumeReceive** to as a last resort in case something happens.




## Assessed type

Other
[M-15] Reentrancy in the BranchPort contract
Audit: 2023-09-maia-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L167-L185
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L188-L219


# Vulnerability details

## Impact
In a Re-entrancy attack, a malicious contract calls back into the calling contract before the first invocation of the function is finished. 
This may cause the different invocations of the function to interact in undesirable ways, especially in cases where the function is updating state variables after the external calls.
This may lead to loss of funds, improper value updates, token loss, etc.

## Proof of Concept
**The replenishReserves function is vulnerable to reentrancy**
```sol
Line 167-185
    function replenishReserves(address _token, uint256 _amount) external override lock {
        // Update Port Strategy Token Debt. Will underflow if not enough debt to repay.
        getPortStrategyTokenDebt[msg.sender][_token] -= _amount;


        // Update Strategy Token Global Debt. Will underflow if not enough debt to repay.
        getStrategyTokenDebt[_token] -= _amount;


        // Get current balance of _token
        uint256 currBalance = ERC20(_token).balanceOf(address(this));


        // Withdraw tokens from startegy
        IPortStrategy(msg.sender).withdraw(address(this), _token, _amount);


        // Check if _token balance has increased by _amount
        require(ERC20(_token).balanceOf(address(this)) - currBalance == _amount, "Port Strategy Withdraw Failed");


        // Emit DebtRepaid event
        emit DebtRepaid(msg.sender, _token, _amount);
    }
```
**The replenishReserves function is vulnerable to reentrancy**
```sol
// Line 188-219
    function replenishReserves(address _strategy, address _token) external override lock {
        // Cache Strategy Token Global Debt
        uint256 strategyTokenDebt = getStrategyTokenDebt[_token];


        // Get current balance of _token
        uint256 currBalance = ERC20(_token).balanceOf(address(this));


        // Get reserves lacking
        uint256 reservesLacking = _reservesLacking(strategyTokenDebt, _token, currBalance);


        // Cache Port Strategy Token Debt
        uint256 portStrategyTokenDebt = getPortStrategyTokenDebt[_strategy][_token];


        // Calculate amount to withdraw. The lesser of reserves lacking or Strategy Token Global Debt.
        uint256 amountToWithdraw = portStrategyTokenDebt < reservesLacking ? portStrategyTokenDebt : reservesLacking;


        // Update Port Strategy Token Debt
        getPortStrategyTokenDebt[_strategy][_token] = portStrategyTokenDebt - amountToWithdraw;
        // Update Strategy Token Global Debt
        getStrategyTokenDebt[_token] = strategyTokenDebt - amountToWithdraw;


        // Withdraw tokens from startegy
        IPortStrategy(_strategy).withdraw(address(this), _token, amountToWithdraw);


        // Check if _token balance has increased