smithery.ai

staking

Auto-loaded by defi-auditor agent during Phase 2 when analyzing staking mechanisms. Provides patterns for: reward-per-token accumulator (Synthetix style), lock periods, multi-reward tokens, boosted staking. Critical: precision loss, first staker advantage, late staker dilution, unstake reentrancy.

First seen Mar 30, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,400 B
  • docs SUMMARY.md 313 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Staking Protocol Patterns

This skill provides comprehensive knowledge for auditing DeFi staking and reward distribution mechanisms.

Common Staking Patterns

1. Simple Staking (No Rewards)

contract SimpleStaking {
    mapping(address => uint256) public staked;

    function stake(uint256 amount) external {
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
    }

    function unstake(uint256 amount) external {
        require(staked[msg.sender] >= amount);
        staked[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
    }
}

2. Reward Per Token Accumulator (Synthetix Style)

contract StakingRewards {
    uint256 public rewardPerTokenStored;
    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) return rewardPerTokenStored;
        return rewardPerTokenStored +
            (rewardRate * (block.timestamp - lastUpdateTime) * 1e18 / totalSupply);
    }

    function earned(address account) public view returns (uint256) {
        return (balanceOf[account] *
            (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18) +
            rewards[account];
    }

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }
}

Reward Distribution Vulnerabilities

1. Precision Loss

Vulnerable Pattern:

// DANGEROUS: Low precision accumulator
function rewardPerToken() public view returns (uint256) {
    // If rewardRate * timeDelta < totalSupply, result is 0!
    return rewardPerTokenStored +
        (rewardRate * (block.timestamp - lastUpdateTime) / totalSupply);
}

Secure Pattern:

// Use high precision (1e18 or 1e27)
function rewardPerToken() public view returns (uint256) {
    return rewardPerTokenStored +
        (rewardRate * (block.timestamp - lastUpdateTime) * 1e18 / totalSupply);
}

2. First Staker Advantage

Vulnerable Pattern:

// DANGEROUS: First staker gets all pending rewards
function stake(uint256 amount) external {
    // If totalSupply was 0, rewardPerTokenStored didn't update
    // First staker claims all rewards since lastUpdateTime
    _updateReward(msg.sender);
    // ...
}

Secure Pattern:

function stake(uint256 amount) external {
    _updateReward(msg.sender);
    // If first staker, reset lastUpdateTime
    if (totalSupply == 0) {
        lastUpdateTime = block.timestamp;
    }
    // ...
}

3. Late Staker Dilution Attack

1. Attacker waits for rewards to accumulate
2. Stakes large amount just before reward distribution
3. Claims disproportionate rewards
4. Unstakes immediately

Mitigation: Time-weighted rewards or lock periods

4. Reward Calculation Overflow

Vulnerable Pattern:

// DANGEROUS: Can overflow with large values
uint256 pending = balance * rewardPerToken / 1e18;
// If balance and rewardPerToken are both large...

Secure Pattern:

// Use mulDiv to avoid overflow
uint256 pending = FullMath.mulDiv(balance, rewardPerToken, 1e18);

Lock Period Vulnerabilities

1. Lockup Bypass

Vulnerable Pattern:

// DANGEROUS: Lock only checked on unstake
function unstake(uint256 amount) external {
    require(block.timestamp >= lockEnd[msg.sender], "Locked");
    // But user can transfer stake tokens!
}

Secure Pattern:

// Non-transferable during lock, or track per-deposit locks
function _beforeTokenTransfer(address from, address to, uint256) internal {
    if (from != address(0) && to != address(0)) {
        require(block.timestamp >= lockEnd[from], "Locked");
    }
}

2. Lock Extension Manipulation

// If lock period extends on additional stake
// User might be tricked into longer lock than expected

// Always show clear lock end time before stake

Unstaking Vulnerabilities

1. Unstake Reentrancy

Vulnerable Pattern:

// DANGEROUS: Reward sent before state update
function unstake(uint256 amount) external {
    uint256 reward = earned(msg.sender);
    rewardToken.transfer(msg.sender, reward); // External call!
    balances[msg.sender] -= amount; // State update after
}

Secure Pattern:

function unstake(uint256 amount) external nonReentrant {
    uint256 reward = earned(msg.sender);
    balances[msg.sender] -= amount; // State first
    rewards[msg.sender] = 0;
    rewardToken.transfer(msg.sender, reward);
    stakedToken.transfer(msg.sender, amount);
}

2. Dust Stuck in Contract

// Rounding can leave tiny amounts stuck
// Ensure users can withdraw their full balance

function unstake(uint256 amount) external {
    if (amount == type(uint256).max) {
        amount = balances[msg.sender];
    }
    // ...
}

Multi-Reward Token Patterns

Multiple Reward Accumulators

contract MultiRewards {
    struct Reward {
        uint256 rewardPerTokenStored;
        uint256 rewardRate;
        uint256 lastUpdateTime;
    }

    mapping(address => Reward) public rewardData;
    mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid;

    function earned(address account, address rewardToken)
        public view returns (uint256)
    {
        // Similar to single reward but per token
    }
}

Vulnerability: Different Update Frequencies

// If reward tokens update at different times
// Can lead to incorrect calculations

// Always update ALL reward tokens together
modifier updateRewards(address account) {
    for (uint i = 0; i < rewardTokens.length; i++) {
        _updateReward(rewardTokens[i], account);
    }
    _;
}

Boosted Staking

veToken / Gauge Patterns

// Boost based on lock duration
function getBoost(address user) public view returns (uint256) {
    uint256 lockDuration = lockEnd[user] - block.timestamp;
    return 1e18 + (lockDuration * maxBoost / maxLockDuration);
}

function earned(address user) public view returns (uint256) {
    uint256 boost = getBoost(user);
    return baseEarned(user) * boost / 1e18;
}

Boost Manipulation

// Attacker repeatedly extends lock to maintain boost
// While others with same stake earn less

// Mitigation: Time-averaged boost or snapshot boost

Staking Audit Checklist

Reward Calculation

  • High precision used (1e18 minimum)
  • No overflow in multiplication
  • Division before multiplication avoided
  • Zero totalSupply handled

First/Late Staker

  • First staker doesn't get all pending rewards
  • Late staker can't dilute existing stakers unfairly
  • Rewards accrue correctly when totalSupply changes

Lock Periods

  • Lock enforced on transfers (not just unstake)
  • Lock extension clearly communicated
  • No lock bypass via stake token transfer

Reentrancy

  • CEI pattern followed
  • nonReentrant modifier on sensitive functions
  • State updated before external calls

Edge Cases

  • Zero stake amount handled
  • Full unstake (max amount) works
  • Dust doesn't get stuck
  • Reward period end handled

Multi-Reward

  • All reward tokens update together
  • Adding/removing reward tokens safe
  • Different decimals handled

Severity Classification

Critical

  • Reentrancy on unstake/claim
  • First staker steals all rewards
  • Precision loss loses significant value

High

  • Lock bypass possible
  • Late staker dilution attack
  • Reward overflow

Medium

  • Dust stuck in contract
  • Boost manipulation
  • Reward token update desync

Liquid Staking Derivatives (LSD) Patterns (2025-2026)

Overview

LSDs (stETH, rETH, cbETH, etc.) introduce unique vulnerabilities when used in DeFi protocols.

1. stETH Rebasing Integration

Vulnerable Pattern:

// DANGEROUS: Caches stETH balance
mapping(address => uint256) public deposits;

function deposit(uint256 amount) external {
    stETH.transferFrom(msg.sender, address(this), amount);
    deposits[msg.sender] = amount;  // @audit Becomes stale after rebase!
}

function withdraw() external {
    uint256 amount = deposits[msg.sender];
    stETH.transfer(msg.sender, amount);  // @audit May not have enough after negative rebase
}

Secure Pattern:

// Use wstETH (wrapped stETH) which doesn't rebase
// Or track shares instead of amounts
function deposit(uint256 amount) external {
    uint256 sharesBefore = stETH.sharesOf(address(this));
    stETH.transferFrom(msg.sender, address(this), amount);
    uint256 sharesReceived = stETH.sharesOf(address(this)) - sharesBefore;
    userShares[msg.sender] += sharesReceived;
}

Search Queries:

Grep("stETH|wstETH|Lido|sharesOf", glob="**/*.sol")

2. LSD Depeg Risk in Lending

Vulnerable Pattern:

// DANGEROUS: Assumes 1:1 peg
function getCollateralValue(address user) view returns (uint256) {
    uint256 stEthAmount = stETH.balanceOf(user);
    return stEthAmount * ethPrice / 1e18;  // @audit Assumes stETH = ETH!
}

Historical Depeg Events:

  • stETH depegged ~5% in June 2022
  • rETH has traded at premiums/discounts
  • cbETH experienced volatility during market stress

Secure Pattern:

function getCollateralValue(address user) view returns (uint256) {
    uint256 stEthAmount = stETH.balanceOf(user);
    uint256 stEthPrice = oracle.getPrice(address(stETH));  // Use actual price
    return stEthAmount * stEthPrice / 1e18;
}

3. Withdrawal Queue Vulnerabilities

LSDs have withdrawal delays:

LSD Withdrawal Time Mechanism
stETH 1-5 days Request queue
rETH Variable Burn for ETH
cbETH Instant* Trade on DEX

*cbETH doesn't have native redemption, only DEX trading

Vulnerable Pattern:

// DANGEROUS: Assumes instant withdrawal
function liquidate(address user) external {
    uint256 stEth = collateral[user];
    stETH.transfer(liquidator, stEth);  // @audit Liquidator can't quickly sell!
}

4. Restaking Integration (EigenLayer)

When LSDs are restaked:

Risk Chain:

ETH → Lido (stETH) → EigenLayer → AVS
      ↓ Risk 1       ↓ Risk 2    ↓ Risk 3
    Slashing      Slashing     Slashing

Vulnerable Pattern:

// DANGEROUS: Not accounting for restaking risk
function acceptCollateral(address token) external view returns (bool) {
    return token == address(stETH);  // @audit Is this stETH restaked? Higher risk!
}

Search Queries:

Grep("EigenLayer|restake|AVS|operator", glob="**/*.sol")
Grep("rETH|cbETH|frxETH|sfrxETH", glob="**/*.sol")

LSD Audit Checklist

Rebasing Tokens (stETH)

  • Uses shares not amounts for accounting
  • Or uses wrapped version (wstETH)
  • Handles negative rebase scenarios

Price/Peg

  • Uses oracle for LSD price (not 1:1 assumption)
  • Circuit breaker for extreme depeg
  • Liquidation accounts for depeg risk

Withdrawals

  • Understands withdrawal delay
  • Liquidation mechanism accounts for illiquidity
  • Emergency scenarios handled

Restaking

  • Knows if LSD is restaked
  • Additional slashing risk priced in
  • Operator risk considered

Search Query Reference

# Find LSD usage
Grep("stETH|wstETH|rETH|cbETH|frxETH|sfrxETH", glob="**/*.sol")
Grep("Lido|RocketPool|Coinbase|Frax", glob="**/*.sol")

# Find rebasing handling
Grep("sharesOf|getSharesByPooledEth|getPooledEthByShares", glob="**/*.sol")

# Find restaking
Grep("EigenLayer|restake|restaking|AVS", glob="**/*.sol")