Summary
Users can still stake stETH in the pool after the pool's maxEndTime, but they won't receive any rewards. Withdrawal of these stETH funds necessitates users to wait for the withdrawLockPeriodAfterStake
duration. In essence, users lock up their stETH for a specific period but do not receive any returns.
Vulnerability Details
Users could still stake even if the pool's maxEndTime
has reached, this will update the lastStake
to the current block.timestamp
.
function _stake(address user_, uint256 poolId_, uint256 amount_, uint256 currentPoolRate_) private {
require(amount_ > 0, "DS: nothing to stake");
Pool storage pool = pools[poolId_];
PoolData storage poolData = poolsData[poolId_];
UserData storage userData = usersData[user_][poolId_];
if (pool.isPublic) {
uint256 balanceBefore_ = IERC20(depositToken).balanceOf(address(this));
IERC20(depositToken).safeTransferFrom(_msgSender(), address(this), amount_);
uint256 balanceAfter_ = IERC20(depositToken).balanceOf(address(this));
amount_ = balanceAfter_ - balanceBefore_;
require(userData.deposited + amount_ >= pool.minimalStake, "DS: amount too low");
totalDepositedInPublicPools += amount_;
}
userData.pendingRewards = _getCurrentUserReward(currentPoolRate_, userData);
poolData.lastUpdate = uint128(block.timestamp);
poolData.rate = currentPoolRate_;
poolData.totalDeposited += amount_;
userData.lastStake = uint128(block.timestamp);
userData.rate = currentPoolRate_;
userData.deposited += amount_;
emit UserStaked(poolId_, user_, amount_);
}
Users are unable to receive any rewards as the reward becomes zero after the maxEndTime
. Additionally, users must wait an extra withdrawLockPeriodAfterStake
duration to withdraw their stETH.
function _withdraw(address user_, uint256 poolId_, uint256 amount_, uint256 currentPoolRate_) private {
Pool storage pool = pools[poolId_];
PoolData storage poolData = poolsData[poolId_];
UserData storage userData = usersData[user_][poolId_];
uint256 deposited_ = userData.deposited;
require(deposited_ > 0, "DS: user isn't staked");
if (amount_ > deposited_) {
amount_ = deposited_;
}
uint256 newDeposited_;
if (pool.isPublic) {
require(
block.timestamp < pool.payoutStart ||
(block.timestamp > pool.payoutStart + pool.withdrawLockPeriod &&
block.timestamp > userData.lastStake + pool.withdrawLockPeriodAfterStake),
"DS: pool withdraw is locked"
);
[...]
}
POC:
Add the test to test/Distribution.test.ts
and run it with npx hardhat test
.
@@ -1387,6 +1387,14 @@ describe('Distribution', () => {
await expect(distribution.withdraw(poolId, wei(0.1))).to.be.revertedWith('DS: pool withdraw is locked');
});
+
+ it.only("revert when withdrawing, although stake after endTime", async () => {
+ await setNextTime(oneDay * 51);
+
+ await distribution.stake(poolId, wei(1));
+
+ await expect(distribution.withdraw(poolId, wei(0.1))).to.be.revertedWith('DS: pool withdraw is locked');
+ });
});
describe('#removeUpgradeability', () => {
Impact
Users can still stake stETH in the pool after the pool's maxEndTime, but they won't receive any rewards. Withdrawal of these stETH funds necessitates users to wait for the withdrawLockPeriodAfterStake
duration. In essence, users lock up their stETH for a specific period but do not receive any returns.
Tools Used
hardhat
Recommendations
Disallow users from staking in pools that have reached maxEndTime.