Reward and Incentive Manipulation
Detects vulnerabilities in staking, yield farming, and reward distribution functions that allow attackers to inflate reward claims through flash loans, timestamp manipulation, or division-before-multiplication precision loss.
Reward and Incentive Manipulation
Overview
Remediation Guide: How to Fix Reward and Incentive Manipulation
The reward manipulation detector identifies vulnerabilities in staking, yield farming, and incentive distribution functions. These functions are high-value targets because they frequently calculate payouts based on instantaneous pool state — balance reads, totalSupply() calls, or block timestamps — that attackers can manipulate before the calculation executes.
Sigvex identifies three primary vulnerability patterns in reward-related functions (any function whose name contains keywords such as reward, claim, harvest, stake, deposit, withdraw, yield, earn, distribute, or incentive):
-
Timestamp-based reward calculation: When
block.timestampis used in arithmetic operations to compute elapsed time and the result drives a reward payout. Validators can manipulate block timestamps within a 900-second window, and attackers can time transactions to maximize per-block rewards. -
Division before multiplication (precision loss): When reward math performs division before multiplication — for example, computing
(elapsed / period) * rate— the intermediate division truncates fractional periods. In large-scale protocols this systematic rounding can be exploited to drain rewards or cause underpayment that accumulates over time. -
Flash loan-vulnerable pool state: When a reward function reads live pool state (storage balances,
ETH.balance, or calls tototalSupply()) and distributes rewards based on that state without first committing the updated accounting to storage. An attacker can borrow assets via flash loan, temporarily inflate the pool, call the harvest function to claim inflated rewards, then repay the loan — all within a single transaction.
Why This Is an Issue
DeFi reward mechanisms have been exploited for $69M+ in annual losses:
- Harvest Finance (2020): $34M. Attackers used flash loans to manipulate USDC/USDT pool ratios, then called the
doHardWork()harvest function to collect inflated yields before repaying. - Cream Finance (2021): $130M. Reward calculation errors in the lending and reward distribution allowed repeated exploitation over multiple transactions.
- Rari Capital (2022): $80M. Flash loan-driven manipulation of reward-bearing pool balances enabled disproportionate reward extraction.
The root cause in all three cases was that reward calculations were based on instantaneous, manipulable state rather than time-weighted or snapshotted values.
How to Resolve
// Before: Vulnerable — reward calculated from live pool state before state is updated
contract VulnerableStaking {
IERC20 public rewardToken;
mapping(address => uint256) public stakedAt;
uint256 public totalStaked;
function harvest() external {
uint256 poolBalance = rewardToken.balanceOf(address(this));
// VULNERABLE: poolBalance is manipulable via flash loan
uint256 reward = (poolBalance * 1e18) / totalStaked;
rewardToken.transfer(msg.sender, reward);
// State updated after transfer — classic reentrancy + flash loan risk
}
}
// After: Update accounting before reading pool state; use CEI pattern
contract SecureStaking {
IERC20 public rewardToken;
uint256 public rewardPerTokenStored;
uint256 public lastUpdateTime;
uint256 public totalStaked;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
modifier updateReward(address account) {
// Commit updated accounting BEFORE any external state read or transfer
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
// Use stored accumulated rewards, not live pool balance
return rewardPerTokenStored
+ ((block.timestamp - lastUpdateTime) * REWARD_RATE * 1e18) / totalStaked;
}
function earned(address account) public view returns (uint256) {
return
(stakedBalance[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
+ rewards[account];
}
// CEI: update state first, then transfer
function harvest() external updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
require(reward > 0, "No rewards");
rewards[msg.sender] = 0; // Effects: clear before transfer
rewardToken.transfer(msg.sender, reward); // Interaction: transfer last
}
}
Examples
Vulnerable Code
contract VulnerableYieldFarm {
IERC20 public depositToken;
IERC20 public rewardToken;
mapping(address => uint256) public deposited;
uint256 public totalDeposited;
function deposit(uint256 amount) external {
depositToken.transferFrom(msg.sender, address(this), amount);
deposited[msg.sender] += amount;
totalDeposited += amount;
}
function harvest() external {
// VULNERABLE: uses live balance — manipulable by flash loan
uint256 poolBalance = rewardToken.balanceOf(address(this));
// VULNERABLE: division before multiplication causes precision loss
// If poolBalance = 1000e18 and totalDeposited = 3000e18:
// deposited[msg] = 1000 → share = 1000/3000 = 0 (truncated)
uint256 userShare = deposited[msg.sender] / totalDeposited;
uint256 reward = userShare * poolBalance;
// Transfer with no prior state update — reentrancy-prone
rewardToken.transfer(msg.sender, reward);
}
}
Fixed Code
contract SecureYieldFarm {
IERC20 public depositToken;
IERC20 public rewardToken;
uint256 public constant PRECISION = 1e18;
uint256 public rewardPerTokenAccumulated;
uint256 public lastRewardTime;
uint256 public rewardRatePerSecond;
uint256 public totalDeposited;
mapping(address => uint256) public deposited;
mapping(address => uint256) public rewardDebt;
mapping(address => uint256) public pendingRewards;
modifier syncRewards(address user) {
// Always commit reward accounting before any state change
uint256 rpt = currentRewardPerToken();
rewardPerTokenAccumulated = rpt;
lastRewardTime = block.timestamp;
if (user != address(0)) {
pendingRewards[user] += (deposited[user] * (rpt - rewardDebt[user])) / PRECISION;
rewardDebt[user] = rpt;
}
_;
}
function currentRewardPerToken() public view returns (uint256) {
if (totalDeposited == 0) return rewardPerTokenAccumulated;
uint256 elapsed = block.timestamp - lastRewardTime;
// Multiply BEFORE dividing to preserve precision
return rewardPerTokenAccumulated + (elapsed * rewardRatePerSecond * PRECISION) / totalDeposited;
}
function harvest() external syncRewards(msg.sender) {
uint256 reward = pendingRewards[msg.sender];
require(reward > 0, "Nothing to harvest");
// Effects first
pendingRewards[msg.sender] = 0;
// Interaction last
rewardToken.transfer(msg.sender, reward);
}
}
Sample Sigvex Output
{
"detector_id": "reward-manipulation",
"severity": "critical",
"confidence": 0.70,
"description": "Function harvest() reads pool state via rewardToken.balanceOf() without first updating reward accounting. An attacker can use a flash loan to temporarily inflate the pool balance, call harvest() to claim inflated rewards, and repay the loan in a single transaction. This pattern matches the $34M Harvest Finance exploit.",
"location": { "function": "harvest()", "offset": 0 }
}
Detection Methodology
Sigvex identifies reward manipulation vulnerabilities through the following steps:
- Function scope filtering: Identifies reward-related functions by matching function names against a keyword list (reward, claim, harvest, stake, deposit, withdraw, yield, earn, distribute, mint, incentive). Only these functions are analyzed.
- Pattern 1 — Timestamp arithmetic: Checks whether
block.timestampis stored in a variable that then appears as an operand in an arithmetic operation (add, subtract, multiply, divide, modulo). Confidence: 0.70. - Pattern 2 — Division-before-multiplication: Tracks division result variables and flags any subsequent multiplication that uses a division result as an operand. Confidence: 0.78 (without overflow protection); reduced when Solidity 0.8+ overflow semantics or SafeMath is detected.
- Pattern 3 — Flash loan-vulnerable pool state: Checks whether the function reads live pool state (storage load,
BALANCE,SELFBALANCE, or external calls) and then performs an external call (transfer or send) without an intervening storage write that commits the updated reward accounting. Confidence: 0.70.
Context modifiers:
- Known audited library (OpenZeppelin, Solmate, Solady): confidence multiplied by 0.3
- Reentrancy guard present: confidence multiplied by 0.5 (guards against flash loan reentrancy)
- Access control present: confidence multiplied by 0.7
- Standard token contract (ERC-20/721/1155): confidence multiplied by 0.4
- Proxy contract: confidence multiplied by 0.4 (logic may be in implementation)
Limitations
False positives:
- Protocols that use time-locked or multi-block reward windows are flagged for timestamp use even when a single-block window provides no meaningful manipulation advantage.
- Contracts using OpenZeppelin’s
ReentrancyGuardand audited Synthetix-style staking math (already multiply-before-divide) may receive reduced-confidence findings that are true negatives.
False negatives:
- Flash loan protection implemented via a custom non-standard reentrancy guard (e.g., a sentinel storage slot compared at function entry) is not always recognized.
- Protocols where pool state is read from a separate oracle or aggregator contract that internally uses snapshots may be flagged because the external call pattern matches the live-state read signature.
Related Detectors
- Flash Loan — flash loans are the primary vehicle for pool manipulation
- Oracle Manipulation — pool ratio manipulation via oracles
- Vault Inflation — first-depositor share price inflation in ERC-4626 vaults
- Timestamp Dependence — general timestamp manipulation