Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: high
Invalid

Zero Share Minting Vulnerability When First User Burns All Their Tokens in `StakingPool` Contract

Summary

The StakingPool contract is vulnerable to a share minting issue even though the totalShares never becomes zero due to the introduction of DEAD_SHARES. If the first user burns all their tokens, the totalShares will be set to DEAD_SHARES, which leads to a situation where future deposits by users fail to mint sufficient shares. This happens because the ratio between totalStaked and totalShares becomes distorted, causing the share minting formula to return zero or near-zero shares for new deposits.

Vulnerability Details

1. Impact of Setting totalShares to DEAD_SHARES

  • Relevant code in _mintShares:

    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;
    }
  • Issue: When the first user burns all their tokens, totalShares is set to DEAD_SHARES to avoid totalShares being zero. However, this introduces a significant issue in the share calculation for future deposits.

  • Key Problem: With totalShares set to DEAD_SHARES, the calculation for minting shares for future deposits becomes distorted. The minting formula in getSharesByStake() now returns zero or very few shares because the ratio between totalStaked and totalShares becomes unbalanced.

2. Effect on Share Minting Formula

  • The calculation for minting shares is as follows:

    function getSharesByStake(uint256 _amount) public view returns (uint256) {
    uint256 totalStaked = _totalStaked();
    if (totalStaked == 0) {
    return _amount;
    } else {
    return (_amount * totalShares) / totalStaked;
    }
    }
  • After the first user burns all their tokens and totalShares is set to DEAD_SHARES, the ratio between totalShares and totalStaked becomes heavily skewed.

  • Key Issue: When a new user deposits tokens, the formula (_amount * totalShares) / totalStaked results in zero or a very small number of shares being minted because totalShares = DEAD_SHARES, which is disproportionately large relative to totalStaked.

3. Exploit Scenario

  • Step 1: The first user deposits tokens into the pool and mints shares. Afterward, they call the burn() function and burn all their share tokens.

  • Step 2: When totalShares becomes zero, the system assigns DEAD_SHARES to totalShares, maintaining a non-zero value for totalShares.

  • Step 3: A new user deposits tokens. However, the minting logic in getSharesByStake() calculates the new shares as:

    (_amount * DEAD_SHARES) / totalStaked

    Since DEAD_SHARES is significantly larger than the actual staked amount, this results in a calculation that rounds down to zero or very few shares.

  • Result: The new user receives zero or an extremely small number of shares, despite having deposited tokens, rendering the staking system ineffective for future users.

Impact

This issue has a severe impact on the usability and fairness of the staking pool:

  • After the first user burns their tokens, future deposits result in zero or minimal shares being minted due to the disproportionate ratio of totalShares (set to DEAD_SHARES) and totalStaked.

  • Future participants are essentially locked out of earning any meaningful share allocation, breaking the staking pool for any new users.

Tools Used

Recommendations

Adjust getSharesByStake() to Handle DEAD_SHARES Correctly

  • The minting formula should account for cases where totalShares is equal to DEAD_SHARES to prevent it from distorting future share allocations. For example:

    function getSharesByStake(uint256 _amount) public view returns (uint256) {
    uint256 totalStaked = _totalStaked();
    if (totalShares == DEAD_SHARES) {
    return _amount; // Ensure new users get appropriate shares when totalShares is DEAD_SHARES
    } else if (totalStaked == 0) {
    return _amount;
    } else {
    return (_amount * totalShares) / totalStaked;
    }
    }
  • This ensures that if the totalShares is DEAD_SHARES, the calculation returns appropriate shares based on the new deposit amount.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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