Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Valid

Denial of Service (DoS) via Donation Front-Running, Reverting First and consecutive Legitimate Deposits

Summary

This vulnerability arises when an attacker front-runs the first legitimate deposit by using the donateTokens function to deposit a small amount (e.g., 1 wei). This causes the totalStaked variable to increase without minting any shares leaving totalShares at 0. When the legitimate first deposit occurs, the share calculation ((_amount * totalShares) / totalStaked) returns 0. This leads to a revert when the contract attempts to subtract DEAD_SHARES from the deposit(shares) while minting first deposit shares causing the first deposit to always fail.

Vulnerability Details

The attacker monitors the mempool for the first legitimate deposit transaction.
The attacker front-runs this transaction with a call to stakingPool.donateTokens(), depositing a small amount (e.g., 1 wei).

function donateTokens(uint256 _amount) external {
token.safeTransferFrom(msg.sender, address(this), _amount);
totalStaked += _amount;
emit DonateTokens(msg.sender, _amount);
}

The donateTokens function updates totalStaked without minting any shares. Now, totalStaked > 0, but totalShares remains 0.
The legitimate user's deposit transaction is then processed. The PriorityPool contract calls the deposit function in the StakingPool contract:

// In StakingPool contract
function deposit(address _account, uint256 _amount, bytes[] calldata _data) external onlyPriorityPool {
require(strategies.length > 0, "Must be > 0 strategies to stake");
uint256 startingBalance = token.balanceOf(address(this));
if (_amount > 0) {
token.safeTransferFrom(msg.sender, address(this), _amount);
_depositLiquidity(_data);
_mint(_account, _amount);
totalStaked += _amount;
} else {
_depositLiquidity(_data);
}
uint256 endingBalance = token.balanceOf(address(this));
if (endingBalance > startingBalance && endingBalance > unusedDepositLimit)
revert InvalidDeposit();
}

Within the deposit function, the _mint function (inherited from StakingRewardsPool) is called:

// In StakingRewardsPool contract
function _mint(address _recipient, uint256 _amount) internal override {
uint256 sharesToMint = getSharesByStake(_amount);
_mintShares(_recipient, sharesToMint);
emit Transfer(address(0), _recipient, _amount);
}

The _mint function calls getSharesByStake, also defined in StakingRewardsPool

// In StakingRewardsPool contract
function getSharesByStake(uint256 _amount) public view returns (uint256) {
uint256 totalStaked = _totalStaked();
if (totalStaked == 0) {
return _amount;
} else {
return (_amount * totalShares) / totalStaked;
}
}

Since totalStaked > 0 (due to the donation) but totalShares == 0, the calculation (_amount * totalShares) / totalStaked will returns 0 for first deposit
The _mintShares function receives 0 as the sharesToMint value.

uint256 private constant DEAD_SHARES = 10 ** 3;
function _mintShares(address _recipient, uint256 _amount) internal {
require(_recipient != address(0), "Mint to the zero address");
if (totalShares == 0) {
shares[address(0)] = DEAD_SHARES;
totalShares = DEAD_SHARES;
_amount -= DEAD_SHARES;
}
totalShares += _amount;
shares[_recipient] += _amount;
}

When _mintShares is called, the subtraction `_amount(sharesToMint) - DEAD_SHARES` causes the transaction to revert due to an underflow, effectively blocking the first and consecutive legitimate deposits.

Impact

Legitimate users attempting to deposit tokens will have their transactions reverted preventing them from participating in the staking pool.

Tools Used

Manual Analysis

Recommendations

Modify the donateTokens function to block donations when totalShares=0

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

donateTokens() allows a malicious user to manipulate the system in such a way that users may receive 0 shares.

Support

FAQs

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