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