The Standard

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

LiquidationPoolManager.runLiquidation can be frontrun where nearly all fees are stolen

Summary

LiquidationPoolManager.runLiquidation() can be frontrun such that any distributed fees can be largely siphoned off to the frontrunner. This is because the protocol calculates how much rewards to give to the user based on the amount of TST they added in a pending stack. A frontrunner can add TST in a pending stake before liquidation occurs and receive most of those distribution fees.

Vulnerability Details

When LiquidationPoolManager.runLiquidation() is called, fees are distributed via calling LiquidationPool.distributeFees(). We can see that function below:

function distributeFees(uint256 _amount) external onlyManager {
uint256 tstTotal = getTstTotal();
if (tstTotal > 0) {
IERC20(EUROs).safeTransferFrom(msg.sender, address(this), _amount);
for (uint256 i = 0; i < holders.length; i++) {
address _holder = holders[I];
positions[_holder].EUROs += _amount * positions[_holder].TST / tstTotal;
}
for (uint256 i = 0; i < pendingStakes.length; i++) {
// AUDIT: note that pendingStakes will also receive fees based on the total amount of TST they own compared to the rest of the protocol.
pendingStakes[i].EUROs += _amount * pendingStakes[i].TST / tstTotal;
}
}
}

Notice that rewards are distributed pro-rata based on how much TST is deposited into a pendingStake. Because of this, a user can call LiquidationPool.increasePosition() and generate their own pendingStake:

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);
// AUDIT: user can create a pendingStake
pendingStakes.push(PendingStake(msg.sender, block.timestamp, _tstVal, _eurosVal));
addUniqueHolder(msg.sender);
}

This allows them to take a large portion of the fees of a liquidation they are attempting to frontrun. The specific scenario can occur below:

State:

  • 100e18 TST staked

Step 1:

  • hacker increases position by 900e18 TST creating a pendingStake

Step 2:

  • Liquidation occurs where 100e18 EUROs are distributed as fees

Step 3:

  • Liquidation calls LiquidationPool.distributeFees()

  • distributeFees() leads to hacker receiving 90e18 EUROs since they own 90% of the TST (900e18 / 1000e18).

Impact

Distributed fees will be siphoned off to frontrunners who can purchase large amounts of TST and then deposit the TST before a liquidation occurs.

This should be treated as a MEDIUM because the frontrunner must have a substantial amount of TST compared to the total amount of TST staked in the pool and must be ok with holding on to the TST while they wait for their withdrawal request to be accepted.

Tools Used

Manual Review

Recommendations

Consider not distributing fees to pending stakes or only allow distributing fees to pending stakes that weren't created in the past few hours. This will ensure that only existing stakers receive the reward and that no one can game the system.

anyone can frontrun by creating a pendingStake with an exorbiant amount of TST locked in their pendingStake. This will cause them to receive more EUROs than everyone else. Medium bug. Step to attack is increasePosition() and then runLiquidation(). Mediation is to not consider pendingStakes created within time period or not at all as earning reward (they can wait a day).

Updates

Lead Judging Commences

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

frontrun-distrubutefees

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

frontrun-feedist-low

Support

FAQs

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