xanoutas/solidity-security-findings huggingface dataset:
Cross-Chain Token Cap Disparity
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/token/ERC20hTokenRoot.sol#L51-L61


# Vulnerability details

## Impact
Potential loss of token value when minted values exceed the uint capacity of target chains, leading to incorrect token representation and financial discrepancies.

## Proof of Concept
This function mints tokens or at least a representation of other chain tokens, the problem is that other chains have different max uints, for example Rust based smart contracts [e.g. Substrate] are max u128, taproot BRCS are uint64_max and other chains may have different ones. So naturally when minted more than other chains maximum the value will be lost because it will be capped.

## Tools Used
Manual

## Recommended Mitigation Steps
Implement dynamic uint boundary checks depending on which chain being used.


## Assessed type

ERC20
VirtualAccount.sol:payableCall() lack requiresApprovedCaller modifier
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
Anyone can use a user's virtual account payableCall and calls functions like retrySettlement/redeemSettlement/retrieveSettlement that require the caller to be the virtual account, which can lead to economic loss to users.

## Proof of Concept
In VirtualAccount.sol, we have two functions which aggregate calls ensuring each call is successful: `call` and `payableCall`. `payableCall` is added later since https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/VirtualAccount.sol#L41.

To prevent anyone use the aggregate call in VirtualAccount, we use the requiresApprovedCaller modifier which only let port approved address call:

```solidity
    modifier requiresApprovedCaller() {
        if (!IRootPort(localPortAddress).isRouterApproved(this, msg.sender)) {
            if (msg.sender != userAddress) {
                revert UnauthorizedCaller();
            }
        }
        _;
    }
```

However, the `payableCall` didn't use requiresApprovedCaller modifier, so anyone can call this function:

```solidity
    /// @inheritdoc IVirtualAccount
    function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) {
```

So, anyone can call a user's virtual account payableCall and trigger privileged functions of that user. For example:

1. Alice triggers a settlement through the root agent bridge using her virtual account.
2. The settlement failed on the dst chain.
3. Time passed, and based on the current situation, Alice decides to retrieve the settlement.
3. However, Bob calls retrySettlement function first using Alice's virtual account `payableCall`, and the settlement is executed success on the dst chain.
4. Alice suffers from the settlement.


## Tools Used
Manual Review.

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


## Assessed type

Access Control
users funds will be stuck in the ```BaseBranchRouter``` when making deposit
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L514-L534
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L175
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L824


# Vulnerability details

## Impact
Users are debited twice the intended amount of every underlying token deposit made at the branch port while locking deposits into the branch port. The first deposit is transferred into the ```BaseBranchRouter``` and the second into the ```BranchPort```. The funds deposited into the ```BaseBranchRouter``` is stuck.


## Proof of Concept
The ```BaseBranchRouter``` is the entry point for users depositing into and withdrawing from the ```BranchPort```.

When a user calls [callOutAndBridge(...)](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L88) function in the ```BaseBranchRouter``` to make a single asset deposit,
```solidity
 function callOutAndBridge(bytes calldata _params, DepositInput calldata _dParams, GasParams calldata _gParams)
        external
        payable
        override
        lock
    {
        //Transfer tokens to this contract.
        // transfer token into a 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 [_transferAndApproveToken(...)](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L160-L177) function is called internally and the user's (```depositor```) funds are transferred from the user to the ```BaseBranchRouter``` and [approval](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BaseBranchRouter.sol#L175) is given to the localPort to spend the users deposited funds on behalf of the router

```solidity
    function _transferAndApproveToken(address _hToken, address _token, uint256 _amount, uint256 _deposit) internal {
        ...
        if (_amount - _deposit > 0) {
            unchecked {
                _hToken.safeTransferFrom(msg.sender, address(this), _amount - _deposit);
                ERC20(_hToken).approve(_localPortAddress, _amount - _deposit);
            }
        }

        // Check if the underlying tokens are being spent
        if (_deposit > 0) {
            _token.safeTransferFrom(msg.sender, address(this), _deposit);
            ERC20(_token).approve(_localPortAddress, _deposit);
        }
    }
```

which then calls ```callOutAndBridge(...)``` of the [BranchBridgeAgent](https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src
VirtualAccount::payableCall is missing access control
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

The lack of access control in VirtualAccount::payableCall function allows anyone to execute arbitrary calls with any contract from a user's VirtualAccount. This enables malicious actors to steal user's funds easily, ultimately resulting in a PublicVirtualAccount.

## Proof of Concept

```solidity
    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();
    }
```

As per of the readme the VirtualAccount should be able to hold ERC20's and ERC721's tokens, hence not posssible with a public `payableCall()`

## Tools Used

Manual review

## Recommended Mitigation Steps

Add `requiresApprovedCaller` to `payableCall()`.


## Assessed type

Access Control
payableCall in VirtualAccount isn’t protected by requiresApprovedCaller modifier 
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
due to the lack of access control in `payableCall` function in VirtualAccount contract, anyone can call `payableCall` on a user virtual account . An attacker can use this vulnerability to steal the users funds after a failed settlement by the user .

## Proof of Concept

- User settlement transaction fails on destination chain for any reason .
- Attacker front-runs user `retrySettlement` transaction by calling `payableCall` on the user virtual Account with target as `RootBridgeAgentAddress` and callData as `retrySettlement` selector and same settlement nonce as the user’s one and the recipient as his address on branch chain to bypass the [caller check in `retrySettlement`](https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L248) .
- the transaction succeed in the branch chain and the attacker receives the funds on the address he put on `retrySettlement`  call .

## Tools Used
Manual review
## Recommended Mitigation Steps
Protect `payableCall` with `requiresApprovedCaller` modifier


## Assessed type

Access Control
`CoreRootRouter.executeDepositSingle` FUNCTION REVERTS FOR SINGLE ASSETS DEPOSITS THUS FAILING THE TRANSACTION
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L504-L510
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L100-L105
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L350-L357
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L134-L139


# Vulnerability details

## Impact

The `RootBridgeAgent.lzReceiveNonBlocking` function calls the respective `RootBridgeAgentExecutor` functions based on the `flag` parsed via the `_payload[0]`. The `_payload[0] == 0x02` stands for the `Call with Deposit` remote call.

The `RootBridgeAgentExecutor.executeWithDeposit` is called with the `localRouterAddress`, `_payload` and `_srcChainId` as input parameters. The `executeWithDeposit` function which initially construct the `DepositParams` struct and then will bridge in assets from the branch chain to the root chain.

After that the `_payload.lenght` is checked and if the `_payload.length > PARAMS_TKN_SET_SIZE` condition is fulfilled (which means there is additional calldata in the payload) the `IRouter.executeDepositSingle` is called as shown below:

            IRouter(_router).executeDepositSingle{value: msg.value}(
                _payload[PARAMS_TKN_SET_SIZE:], dParams, _srcChainId
            );

But the issue is that `CoreRootRouter.executeDepositSingle` function does not pocess any logic implementation and it reverts the transaction as shown below:

    function executeDepositSingle(bytes memory, DepositParams memory, uint16)
        external
        payable
        override
        requiresExecutor //@audit-info - only the bridge agent executor can call 
    {
        revert();
    }

As a result if the payload in the `RootBridgeAgentExecutor.executeWithDeposit` function has additional calldata ( > PARAMS_TKN_SET_SIZE) then the execute with deposit transaction will fail due to revert.

Hence the remote request to deposit a single asset will fail.

## Proof of Concept

```solidity
            _execute(
                nonce,
                abi.encodeWithSelector(
                    RootBridgeAgentExecutor.executeWithDeposit.selector, localRouterAddress, _payload, srcChainId
                ),
                srcChainId
            );
```

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

```solidity
        if (_payload.length > PARAMS_TKN_SET_SIZE) {
            //Execute remote request
            IRouter(_router).executeDepositSingle{value: msg.value}(
                _payload[PARAMS_TKN_SET_SIZE:], dParams, _srcChainId
            );
        }
```

https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L100-L105

```solidity
    function executeDepositSingle(bytes memory, DepositParams memory, uint16)
        external
        payable
        override
        requiresExecutor
    {
        revert();
    }
```

https://github.com/code-423n4/2023-09
USAGE OF `abi.encodePacked` TO ENCODE DATA COULD LEAD TO `payload` DATA COLLISION IN THE `RootBridgeAgent._createSettlementMultiple` FUNCTION
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L893-L903
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1089-L1099
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L241-L250
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L317-L327


# Vulnerability details

## Impact

The `RootBridgeAgent.callOutAndBridgeMultiple` function is used to settle multiple assets and perform a remote call to a branch chain. `callOutAndBridgeMultiple` function calls the `RootBridgeAgent._createSettlementMultiple` internal function to compute the `payload` required to perform the multiple settlements. 

The `_createSettlementMultiple` function constructs the `payload` using the `abi.encodePacked` as shown below:

        _payload = abi.encodePacked(
            _hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02),
            _recipient,
            uint8(hTokens.length),
            _settlementNonce,
            hTokens,
            tokens,
            _amounts,
            _deposits,
            _params
        );

There are five dynamic data type variables used in the above `abi.encodePacked` computation. They are as follows:

    hTokens : address[]
    tokens : address[]
    _amounts : uint256[]
    _deposits : uint256[]
    _params : bytes

Hence above dynamic data types could lead to `payload` data collision when encoded using the `abi.encodePacked`. This is because the `abi.encodePacked` is used for tight packing of arguments without using the zero padding (to constitute bytes32 data length).

As an example the `payload collision could occur` for different `hToken.lenght` array instances when the `_params` bytes length differs. This is because all of the above mentioned five data types are dynamic data types. Hence this could lead to erroneous accounting of the respective token amounts and token addresses when the encoded data is decoded at the receiving bridge agent. As a result this could break the entire accounting system of the protocol.

Similar vulnerabilities are found in the `RootBridgeAgent._performRetrySettlementCall`, `BranchBridgeAgent.callOutAndBridgeMultiple`, `BranchBridgeAgent.callOutSignedAndBridgeMultiple` functions as well.


## Proof of Concept

```solidity
            payload = abi.encodePacked(
                _hasFallbackToggled ? bytes1(0x82) : bytes1(0x02),
                _recipient,
                uint8(_hTokens.length),
                _settlementNonce,
                _hTokens,
                _tokens,
                _amounts,
                _deposits,
                _params
            );
```

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

```solidity
        _payload = abi.encodePacked(
            _hasFallbackToggled ? bytes1(0x02) & 0x0F : bytes1(0x02),
            _recipient,
            uint8(hTokens.length),
            _settlementNonce,
users can make multiple deposits through the same deposit nonce in branch bridge agent
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When a user retries the deposit there is a check that the deposit owner should be `msg.sender` but there is no check that the deposit failed so user can even retry successful deposits with the same nonce which can affect accounting badly

## Proof of Concept
Paste the following code in `BranchBridgeAgentTest.t.sol` & run `forge test --mt `test_poc_user_can_make_multiple_deposits_with_one_nonce` since this is a unit test and user doesn't have htoken and underlying token balance hence the test reverts but the issue is there

```solidity
    function test_poc_user_can_make_multiple_deposits_with_one_nonce(address _user, uint256 _amount) public {
        // Input restrictions
        if (_user < address(3)) _user = address(3);
        else if (_user == localPortAddress) _user = address(uint160(_user) - 10);

        // Prank into user account
        vm.startPrank(_user);

        // Get some gas.
        vm.deal(_user, 5 ether);

        //GasParams
        GasParams memory gasParams = GasParams(0.5 ether, 0.5 ether);

        //Mint Test tokens.
        underlyingToken.mint(_user, _amount);

        //Approve spend by router
        underlyingToken.approve(address(bRouter), _amount);

        console2.log("Test CallOut Addresses:");
        console2.log(address(testToken), address(underlyingToken));

        // Prepare deposit info
        DepositInput memory depositInput = DepositInput({
            hToken: address(testToken),
            token: address(underlyingToken),
            amount: _amount,
            deposit: _amount
        });

        uint32 depositNonce = bAgent.depositNonce();

        expectLayerZeroSend(
            1 ether,
            abi.encodePacked(
                bytes1(0x02),
                depositNonce,
                depositInput.hToken,
                depositInput.token,
                depositInput.amount,
                depositInput.deposit,
                "testdata"
            ),
            _user,
            gasParams
        );

        //Call Deposit function
        bAgent.callOutAndBridge{value: 1 ether}(payable(_user), "testdata", depositInput, gasParams);


        assertEq(bAgent.depositNonce(), depositNonce + 1);
       
        // assuming user has more tokens to bridge
        expectLayerZeroSend(
            1 ether,
            abi.encodePacked(
                bytes1(0x02),
                depositNonce,
                depositInput.hToken,
                depositInput.token,
                depositInput.amount,
                depositInput.deposit,
                "testdata"
            ),
            _user,
            gasParams
        );
        
        // multiple deposits on the same nonce
        bAgent.retryDeposit{value: 1 ether}(false, depositNonce, "testdata", gasParams, false);
        // Prank out of user account
        vm.stopPrank();
    }
```
`VirtualAccount` as a wallet can be called by anyone
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

A `VirtualAccount` represents a user wallet that allows the user to manage assets and perform remote interactions.

But because `payableCall` method lacks any form of authentication it can be called by anyone.
The call can be also executed directly to the desired `VirtualAccount` instance (as shown by the POC in this report).

## Impact

Anyone can call (directly) any `VirtualAccount` user contract and execute asset movements or any desired interaction in **the name of the user** (wallet).

## Proof of Concept

This line is missing an access modifier:

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

Add the following test to the `MulticallRootRouterTest.t.sol` file :

```solidity
// add this after helper import
import {PayableCall} from "@omni/interfaces/IVirtualAccount.sol";

// add as one of the tests
function testAnyoneCanInteractWithTheUserVirtualAccount() public {
    // userVirtualAccount variable is defined as :
    //userVirtualAccount = address(rootPort.fetchVirtualAccount(address(this)));

    PayableCall[] memory payableCallData = new PayableCall[](1);

    payableCallData[0] =
    PayableCall({target: mockApp, callData: abi.encodeWithSelector(bytes4(keccak256(bytes("distro()")))), value : 0});

    // RLP Encode Calldata
    bytes memory data = encodeCalls(abi.encode(payableCallData));

    hevm.startPrank(address(0x1337));
    // value can be anything ..also 0
    userVirtualAccount.call{value : 0}(abi.encodeWithSignature("payableCall(PayableCall[] data)", data));
    hevm.stopPrank();
}
```

The test will succeed to run and will show that anyone can **directly** call an `VirtualAccount` that makes interactions **on behalf of the user**.

```bash
Running 1 test for test/ulysses-omnichain/MulticallRootRouterTest.t.sol:MulticallRootRouterTest
[PASS] testAnyoneCanInteractWithTheUserVirtualAccount() (gas: 13697)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.68ms

Running 1 test for test/ulysses-omnichain/MulticallRootRouterZippedTest.t.sol:MulticallRootRouterZipTest
[PASS] testAnyoneCanInteractWithTheUserVirtualAccount() (gas: 47730)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.84ms
```

## Tools Used
Manual review

## Recommended Mitigation Steps

Add an access modifier like is done for `VirtualAccount#call` method

```diff
-function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { 
+function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) requiresApprovedCaller { 
```


## Assessed type

Access Control
Unprotected ``payableCall()`` exposes user funds to malicious actors
Audit: 2023-09-maia-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The function ``payableCall()`` is used to make calls on to other contracts from the virtualAccount which is linked to a ``userAddress``. This function utilizes the PayableCall struct show below. 
```solidity
struct PayableCall {
    address target;
    bytes callData;
    uint256 value;
}
```

It allows for specially crafted calldata as well as any target address with any value.
This allows for the theft of any ERC20, ERC721 or ERC1155 tokens owned by the contract through transfers or approvals. 
## Proof of Concept
```solidity
 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;
            }
        }
```
As seen above, the contract does not have any access controls to prevent malicious execution of calls. 
Passing a call with a Struct like this would easily wipe the contract:
```solidity
    PayableCall({
         target: usdt address,
         value: 0,
         calldata: bytes(abi.encodeWithSignature(bytes4(keccak256("transfer(address,uint256)")), 5000e6));
      })
``` 
## Tools Used
Manual Review
## Recommended Mitigation Steps
I recommend implementing the proper access control provided in the contract. The ``requiresApprovedCaller`` modifier.


## Assessed type

Access Control