Stake Challenge

Drain the staking contract by exploiting reward calculation or withdrawal logic.

Vulnerable Code
Analyze the Solidity code below to find the vulnerability.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; // Assumed contract StakeContract { using SafeMath for uint256; IERC20 public stakingToken; IERC20 public rewardToken; // Might be the same as stakingToken uint256 public totalStaked; mapping(address => uint256) public stakedBalance; mapping(address => uint256) public rewardDebt; // Tracks rewards already claimed/accounted for // Simplified reward logic - Vulnerable to reentrancy or precision issues uint256 public rewardRate = 1; // Example rate uint256 public lastUpdateTime; constructor(address _staking, address _reward) { stakingToken = IERC20(_staking); rewardToken = IERC20(_reward); lastUpdateTime = block.timestamp; } function calculateReward(address user) public view returns (uint256) { uint256 timeElapsed = block.timestamp.sub(lastUpdateTime); // Simplified: Reward = balance * time * rate (potential overflow/precision issues) // Real contracts use more complex reward-per-token-staked logic. uint256 reward = stakedBalance[user].mul(timeElapsed).mul(rewardRate); return reward.sub(rewardDebt[user]); // Subtract already claimed rewards } function stake(uint256 amount) public { // Claim pending rewards before staking more claimReward(); stakingToken.transferFrom(msg.sender, address(this), amount); stakedBalance[msg.sender] = stakedBalance[msg.sender].add(amount); totalStaked = totalStaked.add(amount); lastUpdateTime = block.timestamp; // Update time crucial } function withdraw(uint256 amount) public { require(stakedBalance[msg.sender] >= amount, "Insufficient stake"); // Claim pending rewards before withdrawing claimReward(); stakedBalance[msg.sender] = stakedBalance[msg.sender].sub(amount); totalStaked = totalStaked.sub(amount); stakingToken.transfer(msg.sender, amount); // Transfer *after* state updates (good) lastUpdateTime = block.timestamp; } // Vulnerable if reward calculation or transfer happens before state update function claimReward() public { uint256 reward = calculateReward(msg.sender); if (reward > 0) { rewardDebt[msg.sender] = rewardDebt[msg.sender].add(reward); // Update debt rewardToken.transfer(msg.sender, reward); // External call - potential reentrancy point // lastUpdateTime = block.timestamp; // Should update time AFTER transfer? } // Missing update of lastUpdateTime? Or done in stake/withdraw? } }
Submit Explanation
Explain the vulnerability and how to exploit it.
Hints (4)
Just a little peak
Hint 1
Hint 2
Hint 3
Hint 4
Explanation
Discomfort = Learning