Summary
In the TempleGoldStaking.sol
contract, users can stake their tokens for a specified vesting period, during which they earn rewards based on certain calculations. The vesting period implies that users do not have the right to withdraw or sell their tokens until the period ends. This mechanism allows users to stake their tokens, earn rewards during the vesting period, and retrieve their full stake amount at the end.
Vulnerability Details
The withdraw
function in the TempleGoldStaking
contract allows users to withdraw their staked tokens before the vesting period ends. This capability undermines the entire protocol's functionality, as users no longer need to wait for the vesting period to end and can withdraw their tokens at any time.
Impact
Code Snippet
https://github.com/Cyfrin/2024-07-templegold/blob/main/protocol/contracts/templegold/TempleGoldStaking.sol#L276-L279
https://github.com/Cyfrin/2024-07-templegold/blob/main/protocol/contracts/templegold/TempleGoldStaking.sol#L95-L101
function withdraw(uint256 amount, uint256 index, bool claim) external override {
StakeInfo storage _stakeInfo = _stakeInfos[msg.sender][index];
_withdrawFor(_stakeInfo, msg.sender, msg.sender, index, amount, claim, msg.sender);
}
function setVestingPeriod(uint32 _period) external override onlyElevatedAccess {
if (_period < WEEK_LENGTH) { revert CommonEventsAndErrors.InvalidParam(); }
if (rewardData.periodFinish >= block.timestamp) { revert InvalidOperation(); }
vestingPeriod = _period;
emit VestingPeriodSet(_period);
}
POC
forge test --mt test_fortis_stakers_CanWithdraw_TokensBefore_VestingPeriodEnds -vvvv
function test_fortis_stakers_CanWithdraw_TokensBefore_VestingPeriodEnds() public {
skip(3 days);
uint32 _rewardDuration = 16 weeks;
_setVestingPeriod(_rewardDuration);
_setRewardDuration(_rewardDuration);
_setVestingFactor(templeGold);
vm.startPrank(alice);
deal(address(templeToken), alice, 1000 ether, true);
_approve(address(templeToken), address(staking), type(uint).max);
uint256 stakeAmount = 100 ether;
uint256 balancebeforestake = templeToken.balanceOf(alice);
staking.stake(stakeAmount);
skip(8 weeks);
emit Withdrawn(alice, alice, 1, 100 ether);
staking.withdraw(100 ether, 1, false);
uint256 balanceafterwithdraw = templeToken.balanceOf(alice);
assertEq(balancebeforestake , balanceafterwithdraw );
vm.stopPrank();
}
Tools Used
Foundry
Recommendations
Implement a check in the withdraw
function to ensure that users cannot withdraw their tokens before the staking period ends.
uint256 vestfinish;
function setVestingPeriod(uint32 _period) external override onlyElevatedAccess {
if (_period < WEEK_LENGTH) { revert CommonEventsAndErrors.InvalidParam(); }
if (rewardData.periodFinish >= block.timestamp) { revert InvalidOperation(); }
vestingPeriod = _period;
++ vestfinish = block.timestamp + vestingPeriod ;
emit VestingPeriodSet(_period);
}
function withdraw(uint256 amount, uint256 index, bool claim) external override {
++ require(block.timestamp >= vestfinish , "You can't withdraw their staked tokens before vesting period over");
StakeInfo storage _stakeInfo = _stakeInfos[msg.sender][index];
_withdrawFor(_stakeInfo, msg.sender, msg.sender, index, amount, claim, msg.sender);
}