Liquid Staking

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

StakingPool.sol :: donateTokens() allows a malicious user to manipulate the system in such a way that users may receive 0 shares.

Summary

donateTokens() is designed to allow token donations to the protocol. However, it has a critical flaw: it modifies totalStaked without increasing the number of shares. This vulnerability can be exploited by an attacker, resulting in users receiving 0 shares.

Vulnerability Details

deposit() is invoked from the PriorityPool to deposit user tokens into the strategies and mint the corresponding shares accordingly.

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();
}

_mint() is responsible for calculating the number of shares that users will receive. It does this by calling the getSharesByStake() to perform the necessary calculations.

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

As you can see, it uses the standard formula to calculate the shares. However, the issue arises from the donateTokens().

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

As you can see, the donateTokens() is external and can be called by anyone. The problem is that it adds the donated amount to totalStaked without increasing the number of shares. A malicious user could exploit this vulnerability to reduce the number of shares users receive to 0.

This will occur when totalStaked > _amount * totalShares.

POC

To illustrate the problem copy the following POC into staking-pool.test.ts file:

it('deposit can mint 0 sahres', async () => {
const { signers, stakingPool, token, accounts } = await loadFixture(deployFixture)
await token.connect(signers[0]).approve(stakingPool.getAddress(), toEther(1000))
//attacker donate 100 tokens
await stakingPool.donateTokens(toEther(100))
//user deposits
await stakingPool.deposit(accounts[0], toEther(0.1), ['0x'])
expect(await stakingPool.sharesOf(accounts[0])).to.equal(0)
})

Impact

An attacker can manipulate the system in such a way that users end up receiving 0 shares.

Tools Used

Manual review.

Recommendations

There are several approaches to address this problem, but the simplest and most effective solution is to remove the donateTokens().

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 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.

Appeal created

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 7 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.