Summary
The TempleGoldStaking.migrateWithdraw()
function forces the withdrawal of the FULL staking amount and the claim of ALL rewards.
However, the forced claiming of rewards can cause a reversion, preventing some stakers from being migrated.
Vulnerability Details
The TempleGoldStaking.migrateWithdraw()
function forces the withdrawal of the full staking amount and the claiming of all rewards for the given staker.
The claimRewards
flag is always set to TRUE
, which means that rewards will be forced to be claimed in every call of TempleGoldStaking.migrateWithdraw()
.
However, the reward token balance of the staking contract can be less than the total claimable amount from all stakers, resulting in the inability to migrate for some stakers.
function migrateWithdraw(address staker, uint256 index) external override onlyMigrator returns (uint256) {
if (staker == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
StakeInfo storage _stakeInfo = _stakeInfos[staker][index];
uint256 stakerBalance = _stakeInfo.amount;
@> _withdrawFor(_stakeInfo, staker, msg.sender, index, _stakeInfo.amount, true, staker);
return stakerBalance;
}
function _withdrawFor(
StakeInfo storage stakeInfo,
address staker,
address toAddress,
uint256 stakeIndex,
uint256 amount,
@> bool claimRewards,
address rewardsToAddress
) internal updateReward(staker, stakeIndex) {
if (amount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
if (claimRewards) {
@> _getReward(staker, rewardsToAddress, stakeIndex);
}
}
function _getReward(address staker, address rewardsToAddress, uint256 index) internal {
uint256 amount = claimableRewards[staker][index];
if (amount > 0) {
claimableRewards[staker][index] = 0;
@> rewardToken.safeTransfer(rewardsToAddress, amount);
emit RewardPaid(staker, rewardsToAddress, index, amount);
}
}
Impact
Some stakers may be unable to complete the migration process due to insufficient reward token balances. This can leave their funds locked in the old contract.
Proof of Concept
Setup
Working Test Case
function test_unable_to_migrate() public {
address migrator = makeAddr("migrator");
uint32 _vestingPeriod = 16 weeks;
uint256 stakeAmount = 100 ether;
ITempleGoldStaking.Reward memory rewardData;
uint256 TGLDdistribution;
uint256 stakingBalanceBefore;
uint256 stakingBalanceAfter;
{
vm.startPrank(executor);
staking.setMigrator(migrator);
_setVestingPeriod(_vestingPeriod);
_setRewardDuration(_vestingPeriod);
_setVestingFactor(templeGold);
deal(address(templeToken), alice, 1000 ether, true);
deal(address(templeToken), bob, 1000 ether, true);
vm.stopPrank();
}
{
skip(5 days);
vm.startPrank(alice);
_approve(address(templeToken), address(staking), type(uint).max);
stakingBalanceBefore = templeGold.balanceOf(address(staking));
staking.stake(stakeAmount);
stakingBalanceAfter = templeGold.balanceOf(address(staking));
TGLDdistribution += stakingBalanceAfter - stakingBalanceBefore;
vm.stopPrank();
skip(10 days);
stakingBalanceBefore = templeGold.balanceOf(address(staking));
staking.distributeRewards();
stakingBalanceAfter = templeGold.balanceOf(address(staking));
TGLDdistribution += stakingBalanceAfter - stakingBalanceBefore;
}
{
skip(100 days);
stakingBalanceBefore = templeGold.balanceOf(address(staking));
staking.distributeRewards();
stakingBalanceAfter = templeGold.balanceOf(address(staking));
TGLDdistribution += stakingBalanceAfter - stakingBalanceBefore;
vm.startPrank(bob);
_approve(address(templeToken), address(staking), type(uint).max);
stakingBalanceBefore = templeGold.balanceOf(address(staking));
staking.stake(stakeAmount);
stakingBalanceAfter = templeGold.balanceOf(address(staking));
TGLDdistribution += stakingBalanceAfter - stakingBalanceBefore;
vm.stopPrank();
}
uint256 rewardPerToken;
uint256 _vestingRate;
{
rewardData = staking.getRewardData();
vm.warp(rewardData.periodFinish);
vm.startPrank(migrator);
rewardPerToken = staking.rewardPerToken();
_vestingRate = _getVestingRate(bob, 1);
uint256 bobEarned = _getEarned(stakeAmount, rewardPerToken, 0, _vestingRate);
vm.expectEmit(address(staking));
emit RewardPaid(address(bob), address(bob), 1, bobEarned);
staking.migrateWithdraw(bob, 1);
rewardPerToken = staking.rewardPerToken();
_vestingRate = _getVestingRate(alice, 1);
uint256 aliceEarned = _getEarned(stakeAmount, rewardPerToken, 0, _vestingRate);
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(staking), templeGold.balanceOf(address(staking)), aliceEarned));
staking.migrateWithdraw(alice, 1);
vm.stopPrank();
console.log("bobEarned: %s", bobEarned);
console.log("aliceEarned: %s\n", aliceEarned);
console.log("Total Earned: %s", bobEarned + aliceEarned);
console.log("Does Total Earned > TGLDdistribution ?: %s", (bobEarned + aliceEarned) > TGLDdistribution);
}
{
console.log("TGLDdistribution for TemplGoldStaking: %s\n", TGLDdistribution);
ITempleGold.DistributionParams memory params = templeGold.getDistributionParameters();
uint256 MAX_DistributionForTempleGoldStaking = templeGold.MAX_SUPPLY() * params.staking / templeGold.DISTRIBUTION_DIVISOR();
assertEq(TGLDdistribution, MAX_DistributionForTempleGoldStaking);
}
}
Results of running the test:
Ran 1 test for test/forge/templegold/TempleGoldStaking.t.sol:TempleGoldStakingTest
[PASS] test_unable_to_migrate() (gas: 1286931)
Logs:
bobEarned: 170089285714285714282252800
aliceEarned: 170089285714285714282252800
Total Earned: 340178571428571428564505600
Does Total Earned > TGLDdistribution ?: true
TGLDdistribution for TemplGoldStaking: 300000000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 772.06ms (4.59ms CPU time)
Ran 1 test suite in 958.28ms (772.06ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Tools Used
Recommendations
-
Allow flexibility in the claiming reward flag via TempleGoldStaking.migrateWithdraw()
to avoid the issue of insufficient rewards causing a reversion.
-
Update TempleGoldStaking._getReward()
to avoid claiming an amount that exceeds the available balance.
//Location: TempleGoldStaking.sol
- function migrateWithdraw(address staker, uint256 index) external override onlyMigrator returns (uint256) {
+ function migrateWithdraw(address staker, uint256 index, bool claim) external override onlyMigrator returns (uin
if (staker == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
StakeInfo storage _stakeInfo = _stakeInfos[staker][index];
uint256 stakerBalance = _stakeInfo.amount;
- _withdrawFor(_stakeInfo, staker, msg.sender, index, _stakeInfo.amount, true, staker);
+ _withdrawFor(_stakeInfo, staker, msg.sender, index, _stakeInfo.amount, claim, staker);
return stakerBalance;
}