Summary
The migrateWithdraw
function is designed to facilitate the migration of funds to a new staking contract. However, the function fails to update the staker's balance on the new contract after moving the funds, leading to potential discrepancies and loss of staked balances for users.
https://github.com/TempleDAO/temple/blob/templegold/protocol/contracts/templegold/TempleGoldStaking.sol#L168
Vulnerability Details
The function correctly transfers the staked funds to the new staking contract but omits the crucial step of updating the staker's balance on the new contract. As a result, the new staking contract does not reflect the migrated staked amount for the user, causing inconsistencies and potential loss of staked funds.
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(); }
uint256 _stakeAmount = stakeInfo.amount;
if (_stakeAmount < amount)
{ revert CommonEventsAndErrors.InsufficientBalance(address(stakingToken), amount, _stakeAmount); }
unchecked {
stakeInfo.amount = _stakeAmount - amount;
}
_balances[staker] -= amount;
totalSupply -= amount;
_moveDelegates(delegates[staker], address(0), amount);
stakingToken.safeTransfer(toAddress, amount);
emit Withdrawn(staker, toAddress, stakeIndex, amount);
if (claimRewards) {
_getReward(staker, rewardsToAddress, stakeIndex);
}
}
Proof of Concept
-
Executor calls setMigrator on the old staking contract, passes the new staking contract as the migrator.
-
Bob stakes 100 Temple Token
-
After a few days, Someone calls distributeRewards
-
After a few days, bob checks staking balance and it is looking good.
-
Protocol decides to migrate to a new staking contract
-
Protocol calls migrateWithdraw for bob stake
-
The funds successfully moved to the new staking contract but bob stakes were not updated in the new staking contract.
Proof of Code
vm.startPrank(executor);
staking.setMigrator(address(mockStaking));
uint256 _rewardDuration = 16 weeks;
uint32 _vestingPeriod = uint32(_rewardDuration);
_setVestingFactor(templeGold);
_setVestingPeriod(_vestingPeriod);
_setRewardDuration(_rewardDuration);
vm.startPrank(bob);
deal(address(templeToken), bob, 1000 ether, true);
_approve(address(templeToken), address(staking), type(uint).max);
staking.stake(100 ether);
skip(2 days);
staking.distributeRewards();
skip(2 days);
uint256 bobStakingBalance = staking.balanceOf(bob);
vm.startPrank(address(mockStaking));
staking.migrateWithdraw(bob, 1);
uint256 bobNewStakingBalance = mockStaking.balanceOf(bob);
assert(bobStakingBalance != bobNewStakingBalance);
vm.startPrank(bob);
vm.expectRevert();
mockStaking.withdraw(bobStakingBalance, 1, false);
Impact
Users will experience a loss of staked funds, leading to a potential financial loss.
Tools Used
Manual Review
Recommendations
Modify the migrateWithdraw
or other neccesary function to include the step of updating the staker's balance on the new staking contract. Ensure that the staker's balance is accurately reflected in the new contract after migration.