skills/protocol-patterns/staking-vesting/SKILL.md
Staking security guidance for reward accounting, lock periods, timing attacks, and withdrawals.
npx skillsauth add apegurus/solidity-argus staking-vestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Staking protocols lock tokens to earn rewards. Core security concerns: reward calculation, timing attacks, withdrawal delays, and token accounting.
┌─────────────────────────────────────────────────────────────┐
│ STAKING PROTOCOL │
├─────────────────────────────────────────────────────────────┤
│ │
│ STAKE EARN UNSTAKE │
│ ────── ──── ─────── │
│ Lock tokens → Accrue rewards → Withdraw + rewards │
│ │
│ rewardPerToken = totalRewards / totalStaked / time │
│ │
│ userReward = (rewardPerToken - userRewardPaid) * balance │
│ │
└─────────────────────────────────────────────────────────────┘
Attack Vectors:
Checklist:
// Standard Synthetix staking pattern
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function earned(address account) public view returns (uint256) {
return balances[account] *
(rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18 +
rewards[account];
}
Attack Vectors:
Checklist:
// VULNERABLE: Instant stake and claim
function distributeReward(uint256 amount) external {
rewardToken.transferFrom(msg.sender, address(this), amount);
rewardPerTokenStored += amount * 1e18 / totalStaked;
// Flash staker can stake before this, claim after
}
// SECURE: Drip rewards over time
function notifyRewardAmount(uint256 reward) external {
if (block.timestamp >= periodFinish) {
rewardRate = reward / DURATION;
} else {
uint256 remaining = periodFinish - block.timestamp;
uint256 leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / DURATION;
}
lastUpdateTime = block.timestamp;
periodFinish = block.timestamp + DURATION;
}
Attack Vectors:
Checklist:
// VULNERABLE: Assumes full amount received
function stake(uint256 amount) external {
stakingToken.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount; // Wrong for fee-on-transfer!
totalStaked += amount;
}
// SECURE: Check actual amount received
function stake(uint256 amount) external {
uint256 balanceBefore = stakingToken.balanceOf(address(this));
stakingToken.transferFrom(msg.sender, address(this), amount);
uint256 received = stakingToken.balanceOf(address(this)) - balanceBefore;
balances[msg.sender] += received;
totalStaked += received;
}
Attack Vectors:
Checklist:
// VULNERABLE: Lock can be extended/reset incorrectly
function stake(uint256 amount) external {
balances[msg.sender] += amount;
lockEnd[msg.sender] = block.timestamp + LOCK_PERIOD; // Resets lock!
}
// SECURE: Don't reset existing locks
function stake(uint256 amount) external {
balances[msg.sender] += amount;
if (lockEnd[msg.sender] < block.timestamp) {
lockEnd[msg.sender] = block.timestamp + LOCK_PERIOD;
}
// Existing lock preserved
}
Attack Vectors:
Checklist:
// VULNERABLE: External call before state update
function claim() external {
uint256 reward = earned(msg.sender);
rewardToken.transfer(msg.sender, reward); // External call first
rewards[msg.sender] = 0; // State update after
}
// VULNERABLE: Precision loss
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
return rewardPerTokenStored +
(rewardRate / totalStaked) * (block.timestamp - lastUpdate); // Wrong order!
}
// SECURE
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
return rewardPerTokenStored +
rewardRate * (block.timestamp - lastUpdate) * 1e18 / totalStaked;
}
testing
Specialist profile for mechanically applying the attack-vector deck and classifying vectors as skip, drop, or investigate.
tools
Specialist profile for libraries, helpers, base contracts, adapters, encoders, wrappers, and integration glue.
testing
Specialist profile for rounding, scale, decimal, downcast, and arithmetic accounting edge cases.
testing
Specialist profile for extracting conservation laws and state couplings, then searching for violating paths.