stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: medium
Invalid

Users Have to Wait an Unfair Time to Withdraw Their Tokens, When Their Lock Duration Has Finished.

Summary

Users Who try to withdraw their Tokens after the Duration Lock has time Finished are Unable to Withdraw and have to Ask for an Unlock and Wait for the Expiry period, When They should not wait any more time.

Vulnerability Details

In the Stake.link protocol any user can deposit SDL tokens and other tokens to stake and win rewards from the protocol, the users can stake tokens and if they don't lock the tokens deposited in the protocol, they can withdraw these tokens at any time they want, but if the user decides to lock their tokens for a determined time they win the rewards plus some extra rewards (Boosted rewards) this incentivize users to lock their tokens in the protocol.

When any User decides to withdraw his tokens, they can withdraw immediately if they didn't lock their tokens in the protocol, but if they lock their tokens they have to ask to unlock the tokens first, then they should wait for half of the Lock Duration time to be able to call the Withdraw function to get their tokens back, this is the process that the user should follow for withdrawing tokens from the protocol, but this process has a flag that causes that any user who locks their tokens in the protocol has to wait an unfair amount of time to withdraw their tokens from the protocol.

Follow me in the next example, where I show you guys this problem:

1.- A bunch of users deposit in the protocol SDL tokens and other tokens allowed by the protocol. but as they want to receive boosted rewards they lock their tokens for a couple of years or maybe the max lock duration of 4 years.

2.- The time starts passing and the users are winning rewards plus boosted rewards from the protocol and everything is great.

3.- Then more than 2 years have passed and the users who lock their tokens are urgently needing the money for any personal reasons, so they decide to withdraw the tokens from the protocol.

4.- Since the Lock Duration time they set in the beginning has ended, they try to withdraw the tokens immediately, but they notice they have to ask for an unlock of their tokens even when the lock time has expired.

5.- After they ask for the unlock they notice they have to wait 1 more year to be able to withdraw their tokens, and the users who lock for 4 years have to wait 2 more years to withdraw their tokens.

This behavior is undesirable and a problem that any user who has already finished their locking time shouldn't face.

The problem is caused because the withdraw function doesn't check if the lock duration time from the lock has passed.

function withdraw(uint256 _lockId, uint256 _amount)
external
onlyLockOwner(_lockId, msg.sender)
updateRewards(msg.sender)
{
if (locks[_lockId].startTime != 0) {
uint64 expiry = locks[_lockId].expiry;
if (expiry == 0) revert UnlockNotInitiated();
if (expiry > block.timestamp) revert TotalDurationNotElapsed();
}
uint256 baseAmount = locks[_lockId].amount;
if (_amount > baseAmount) revert InsufficientBalance();
emit Withdraw(msg.sender, _lockId, _amount);
if (_amount == baseAmount) {
delete locks[_lockId];
delete lockOwners[_lockId];
// balance de locks creados por el usuario, puede crear muchos locks con diferentes amounts
balances[msg.sender] -= 1;
if (tokenApprovals[_lockId] != address(0)) delete tokenApprovals[_lockId];
emit Transfer(msg.sender, address(0), _lockId);
} else {
locks[_lockId].amount = baseAmount - _amount;
}
effectiveBalances[msg.sender] -= _amount;
totalEffectiveBalance -= _amount;
sdlToken.safeTransfer(msg.sender, _amount);
}

To validate this behavior I created a test, you can add this test to the test/core/sdlPool/sdl-pool-primary.test.ts file.

it.only('Users Who try to get his Tokens after the Duration Lock has Finished is Unable to Withdraw and has to Ask for Unlock and Wait the Expiry Period, When He should not.', async () => {
// User Locks these tokens for 2 years to earn rewards.
await sdlToken.transferAndCall(
sdlPool.address,
toEther(100),
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 730 * DAY]) // 2 years
)
// Now more than 2 years has passed and the user tries to withdraw his tokens cause he urgently need it for an emergency.
await time.increase(731 * DAY)
// This would revert, cause the user is requiered to initiate an unlock request even when the Lock Duration time set by the user has finished.
await expect(sdlPool.withdraw(1, toEther(100))).to.be.revertedWith('UnlockNotInitiated()')
// The user has to initiate an unlock request even when the Lock Duration time set by the user at the beginning has finished.
await sdlPool.initiateUnlock(1)
let startBalance = await sdlToken.balanceOf(accounts[0])
// The user has to wait Half of the Lock Duration Time, when He shouldn't, cause the Lock Duration time has expired.
await time.increase(365 * DAY)
// If 1 year(half of lock duration) hasn't pass the user is unable to withdraw
//await expect(sdlPool.withdraw(1, toEther(100))).to.be.revertedWith('TotalDurationNotElapsed()')
await sdlPool.withdraw(1, toEther(100))
// Checking all Balances from the User and the Pool are Correct.
assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startBalance)), 100)
assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 0)
assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0)
assert.equal(fromEther(await sdlPool.totalStaked()), 0)
assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 0)
assert.equal(fromEther(await sdlPool.staked(accounts[0])), 0)
})

Impact

This problem force users of the protocol to wait even more time than the lock duration time they set at the beginning, cause their tokens are locked by a half of the lock time duration even after the lock duration time of the user has expired, this is unfair and can be problematic for users.

Tools Used

Manual Code Review, Hardhat Test.

Recommendations

The recommendation would be to add a validation in the Withdraw function to check if the user has met the initially established lock duration time.

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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