code-423n4 / 2023-12-autonolas-findings

3 stars 3 forks source link

possible to mint max uint amount of bridgeERC20 tokens via FxERC20ChildTunnel.deposit() #19

Closed c4-bot-2 closed 10 months ago

c4-bot-2 commented 10 months ago

Lines of code

https://github.com/code-423n4/2023-12-autonolas/blob/2a095eb1f8359be349d23af67089795fb0be4ed1/governance/contracts/bridges/FxERC20ChildTunnel.sol#L96-L106 https://github.com/code-423n4/2023-12-autonolas/blob/2a095eb1f8359be349d23af67089795fb0be4ed1/governance/contracts/bridges/FxERC20RootTunnel.sol#L74-L77

Vulnerability details

Impact

if an erc20 token like cUSDCV3 or with special behaviour similar to cUSDCV3 is added to the FxERC20ChildTunnel bridge, it will be possible to trick the bridge to send a message to FxERC20RootTunnel to mint max uint amount of bridged tokens to the user address. This behaviour happens because tokens like cUSDCV3 send the balance of the holder if the amount to be transferred is specified as type(uint).max instead of reverting. It is always best to check the balance of the user against the amount specified in parameter when intgerating erc20 tokens.

Tokens like this, cUSDCV3 for example have good representation on polygon and ethereum mainnet and tokens with these behaviour have been documented to exist - https://github.com/d-xo/weird-erc20?tab=readme-ov-file#transfer-of-less-than-amount It is better to design

Proof of Concept

https://github.com/compound-finance/comet/blob/22cf923b6263177555272dde8b0791703895517d/contracts/Comet.sol#L929

    function transferInternal(address operator, address src, address dst, address asset, uint amount) internal {
        if (isTransferPaused()) revert Paused();
        if (!hasPermission(src, operator)) revert Unauthorized();
        if (src == dst) revert NoSelfTransfer();

        if (asset == baseToken) {
            if (amount == type(uint256).max) {
                amount = balanceOf(src);
            }
            return transferBase(src, dst, amount);
        } else {
            return transferCollateral(src, dst, asset, safe128(amount));
        }
    }
    function deposit(uint256 amount) external {
        _deposit(msg.sender, amount);
    }

    function _deposit(address to, uint256 amount) internal {
        // Check for the non-zero amount
        if (amount == 0) {
            revert ZeroValue();
        }

        // Encode message for root: (address, address, uint256)
        bytes memory message = abi.encode(msg.sender, to, amount);
        // Send message to root
        _sendMessageToRoot(message);

        // Deposit tokens on an L2 bridge contract (lock)
        bool success = IERC20(childToken).transferFrom(msg.sender, address(this), amount);
        if (!success) {
            revert TransferFailed(childToken, msg.sender, address(this), amount);
        }

        emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount);
    }

    function _sendMessageToRoot(bytes memory message) internal {
        emit MessageSent(message);
    }
    function _processMessageFromChild(bytes memory message) internal override {
        // Decode incoming message from child: (address, address, uint256)
        (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256));

        // Mints bridged amount of tokens to a specified address
        IERC20(rootToken).mint(to, amount);

        emit FxDepositERC20(childToken, rootToken, from, to, amount);
    }

I wrote a POC to illutrate this vuln.

Tools Used

MANUAL REVIEW, FOUNDRY

Recommended Mitigation Steps

    function _deposit(address to, uint256 amount) internal {
        // Check for the non-zero amount
        if (amount == 0) {
            revert ZeroValue();
        }

    if ( IERC20(childToken).balanceOf(msg.sender) < amount) {
          revert InvalidAmount();
    }

        // Encode message for root: (address, address, uint256)
        bytes memory message = abi.encode(msg.sender, to, amount);
        // Send message to root
        _sendMessageToRoot(message);

        // Deposit tokens on an L2 bridge contract (lock)
        bool success = IERC20(childToken).transferFrom(msg.sender, address(this), amount);
        if (!success) {
            revert TransferFailed(childToken, msg.sender, address(this), amount);
        }

        emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount);
    }

if user token balance is less than amount to be transferred, revert function.

Assessed type

ERC20

c4-pre-sort commented 10 months ago

alex-ppg marked the issue as duplicate of #152

c4-pre-sort commented 10 months ago

alex-ppg marked the issue as insufficient quality report

alex-ppg commented 10 months ago

Describes full balance transfer w/ max input relating to non-standard EIP-20 tokens for the FX portal, similar to how #152 describes deflationary / rebasing token incompatibility.

dmvt commented 10 months ago

https://docs.code4rena.com/awarding/judging-criteria/supreme-court-decisions-fall-2023#verdict-unsupported-fee-on-transfer-tokens-and-broader-erc20-discussion

c4-judge commented 10 months ago

dmvt marked the issue as unsatisfactory: Invalid