# audit-staking

Audits Solidity staking and reward protocols for vulnerabilities including front-running first deposit to steal initial rewards, reward dilution via direct transfers, precision loss in reward calculations causing rounding to zero, flash deposit/withdraw griefing diluting rewards, update not called after reward distribution causing stale index, and balance caching issues during claims (project)

- **Kind:** skill
- **Source:** https://github.com/auditmos/skills
- **Page:** https://forefy.com/skills/a7201996-3f4d-458c-a435-254e8b27b259
- **API (JSON + files):** https://forefy.com/api/asr/a7201996-3f4d-458c-a435-254e8b27b259

---

## SKILL.md

---
name: audit-staking
description: Audits Solidity staking and reward protocols for vulnerabilities including front-running first deposit to steal initial rewards, reward dilution via direct transfers, precision loss in reward calculations causing rounding to zero, flash deposit/withdraw griefing diluting rewards, update not called after reward distribution causing stale index, and balance caching issues during claims (project)
allowed-tools: Read, Grep, Glob
license: MIT
compatibility: Designed for Claude Code (or similar products)
metadata:
  author: Tomasz Kowalczyk (tom@auditmos.com)
  version: "1.0"
---

# Staking & Reward Auditor

## When to Use
- Auditing staking mechanisms, reward distribution, yield farming
- User mentions: staking, rewards, yield, farming, rewardPerToken, deposit, withdraw, claim, first depositor
- Analyzing reward calculations, index updates, share dilution
- Reviewing deposit/withdraw flows, precision handling

## Audit Workflow

**IMPORTANT: Announce skill usage at the start of analysis**

Begin with: "I'm using the **audit-staking** skill to analyze this contract for staking and reward vulnerabilities..."

1. **Scan for staking operations**
   - Search: `stake`, `deposit`, `withdraw`, `claim`, `rewardPerToken`, `totalSupply`, `balanceOf`, `earned`, `updateReward`
   - Focus: reward calculations, first depositor, direct transfers, precision loss, flash actions

2. **Check against vulnerability patterns**
   - Reference `reference.md` for complete checklist
   - Compare code against `example.md`

3. **Validate exploitability**
   - **Check access control first** - grep for `onlyOwner|onlyAdmin|onlyGovernance` modifiers
   - Can non-privileged actors exploit? (first depositor steal, direct transfer dilution, flash griefing)
   - Can direct transfers dilute rewards?
   - Do small amounts round to zero?
   - Can flash deposits/withdraws grief stakers?
   - Is update called after distribution?
   - Are balances cached correctly?
   - Verify no compensating protections exist
   - Downgrade severity if admin-only unless direct user impact

4. **Generate report**
   - Use deliverable template below
   - Include reward theft/dilution analysis and PoC
   - Rank by severity

## Core Vulnerability Patterns

See `reference.md` for full checklist. Key patterns:

1. Front-running first deposit → attacker steals initial WETH rewards via sandwich attack
2. Reward dilution via direct transfer → sending tokens directly increases totalSupply without staking
3. Precision loss in rewards → small stakes or frequent updates cause rewards rounding to zero
4. Flash deposit/withdraw griefing → large instant deposits dilute rewards for existing stakers
5. Update not called after distribution → stale index causes incorrect reward calculations
6. Balance caching issues → claiming updates cached balance incorrectly

**Code examples:** See `example.md`

## Severity Criteria

**Critical:** First depositor can steal all initial rewards, direct transfer dilution enabling theft, flash deposit/withdraw draining rewards, **MUST be exploitable by non-privileged actors**
**High:** Precision loss causing rewards to round to zero for legitimate users, stale index after distribution, **MUST be exploitable by non-privileged actors**
**Medium:** Suboptimal reward distribution timing, griefing via large flash actions without theft, admin-only reward configuration issues with user impact
**Low:** Gas inefficiencies in reward calculations, missing events, admin-only parameter issues without immediate user impact

**IMPORTANT:** Admin-only reward functions (onlyOwner, onlyAdmin, onlyGovernance) are **MEDIUM or LOW severity** unless:
- Invalid reward parameters directly steal/brick user rewards
- Missing validation enables admin rug pull of staking pool
- Error cascades to all users immediately (e.g., division by zero in reward calculation)

## False Positives - Do NOT Flag

- Protocols requiring minimum stake amounts (prevents dust)
- Reward tokens same as staking tokens by design
- Intentional admin-only initial deposit
- View functions with documented staleness
- Protocols with explicit front-running protection

## Deliverable Format

**MANDATORY:** Before deliverable, verify each `checklist.md` item against codebase. Flag violations as findings.

Use template: `templates/report-template.md`

Each finding includes: severity, pattern #, file/lines, description, vulnerable code, reward theft analysis, PoC, remediation.

## Key Principles

- **Separate tokens** - reward token must differ from staking token
- **No direct transfers** - track staked amounts separately from balances
- **Precision protection** - minimum stake, scale factors for small amounts
- **Index updates** - call updateReward before and after distribution
- **Flash protection** - time locks or minimum stake duration
- **Balance integrity** - careful caching during claims

## Output Guidelines

**DO:**
- Reference specific lines and functions
- Provide reward theft scenarios with calculations
- Show PoCs demonstrating reward extraction
- Calculate precision loss magnitude
- Map update call timing

**DON'T:**
- Report intentional design choices (same token staking)
- Flag missing features with alternative mechanisms
- Ignore precision implications (critical for accuracy)
- Miss edge cases (first deposit, zero stakes)

## checklist.md

# Staking & Reward Security Checklist

Verify each item before finalizing audit report:

- [ ] **Separate tokens:** Reward token cannot be same as staking token (prevents first depositor attack)
- [ ] **No direct transfer dilution:** totalSupply tracks staked amounts, not token balance
- [ ] **Precision protection:** Minimum stake enforced or sufficient scaling to prevent rounding to zero
- [ ] **Flash protection:** Time locks, minimum duration, or anti-sandwich mechanisms implemented
- [ ] **Index updates:** updateReward called before AND after reward distribution
- [ ] **Balance integrity:** Cached balances updated correctly during claims

## example.md

# Staking & Reward Vulnerability Examples

## Pattern #1: Front-Running First Deposit

### VULNERABLE
```solidity
contract VulnerableFirstDeposit {
    IERC20 public stakingToken; // WETH
    IERC20 public rewardToken; // WETH - SAME TOKEN!

    uint256 public totalSupply;
    uint256 public rewardPerTokenStored;
    mapping(address => uint256) public balances;

    // ISSUE: Reward token same as staking token
    function deposit(uint256 amount) external {
        totalSupply += amount;
        balances[msg.sender] += amount;
        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function getReward() external {
        uint256 reward = earned(msg.sender);
        rewardToken.transfer(msg.sender, reward);
    }

    // Attack:
    // 1. Protocol deploys with 1000 WETH rewards
    // 2. Attacker front-runs first user's deposit(100 WETH)
    // 3. Attacker deposits 1 wei
    // 4. User's deposit executes
    // 5. Attacker withdraws 1 wei + claims 990 WETH rewards
    // 6. User only gets 10 WETH of original 1000 WETH
}
```

### FIXED
```solidity
contract FixedFirstDeposit {
    IERC20 public stakingToken; // WETH
    IERC20 public rewardToken; // USDC - DIFFERENT TOKEN!

    uint256 public totalSupply;
    mapping(address => uint256) public balances;

    // Reward token different from staking token
    // Initial rewards can't be stolen via first deposit

    function deposit(uint256 amount) external {
        require(amount >= MIN_DEPOSIT, "Amount too small");
        totalSupply += amount;
        balances[msg.sender] += amount;
        stakingToken.transferFrom(msg.sender, address(this), amount);
    }
}
```

## Pattern #2: Reward Dilution via Direct Transfer

### VULNERABLE
```solidity
contract VulnerableDilution {
    IERC20 public stakingToken;
    uint256 public rewardPerTokenStored;

    // ISSUE: Uses token balance for totalSupply
    function rewardPerToken() public view returns (uint256) {
        uint256 totalSupply = stakingToken.balanceOf(address(this));

        if (totalSupply == 0) return rewardPerTokenStored;

        // Attacker sends tokens directly to contract
        // totalSupply inflated without earning rights
        // Dilutes rewards for legitimate stakers

        return rewardPerTokenStored + (newRewards * 1e18 / totalSupply);
    }

    // Attack:
    // 1. 100 users stake 100 tokens each = 10k total
    // 2. 1000 rewards to distribute
    // 3. Attacker sends 10k tokens directly to contract
    // 4. totalSupply now 20k (10k staked + 10k direct)
    // 5. Rewards per token halved
    // 6. Legitimate stakers lose 50% of rewards
}
```

### FIXED
```solidity
contract FixedDilution {
    IERC20 public stakingToken;
    uint256 public totalSupply; // Tracked separately
    uint256 public rewardPerTokenStored;
    mapping(address => uint256) public balances;

    function stake(uint256 amount) external {
        totalSupply += amount; // Only increments through stake
        balances[msg.sender] += amount;
        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) return rewardPerTokenStored;

        // Uses tracked totalSupply, not balance
        // Direct transfers don't affect calculation
        return rewardPerTokenStored + (newRewards * 1e18 / totalSupply);
    }
}
```

## Pattern #3: Precision Loss in Reward Calculation

### VULNERABLE
```solidity
contract VulnerablePrecision {
    uint256 public rewardRate = 1e18; // 1 token per second
    uint256 public totalSupply;
    mapping(address => uint256) public balances;

    function earned(address account) public view returns (uint256) {
        uint256 duration = block.timestamp - lastUpdateTime;

        // ISSUE: Small stakes cause precision loss
        // Example: balance = 10 wei, totalSupply = 1000 ether
        // rewardPerToken = duration * 1e18 / 1000e18 = duration / 1000
        // earned = 10 * (duration / 1000) / 1e18 = 0 (rounds to zero!)

        uint256 rewardPerToken = duration * rewardRate / totalSupply;
        return balances[account] * rewardPerToken / 1e18;
    }

    // User with 10 wei stake earns 0 rewards forever
    // Accumulated loss significant over many users/time
}
```

### FIXED
```solidity
contract FixedPrecision {
    uint256 public rewardRate = 1e18;
    uint256 public totalSupply;
    uint256 public constant MIN_STAKE = 1000e18; // 1000 tokens minimum
    mapping(address => uint256) public balances;

    function stake(uint256 amount) external {
        require(
            balances[msg.sender] + amount >= MIN_STAKE,
            "Below minimum stake"
        );

        totalSupply += amount;
        balances[msg.sender] += amount;
    }

    function earned(address account) public view returns (uint256) {
        uint256 duration = block.timestamp - lastUpdateTime;

        // With minimum stake, precision loss avoided
        // Minimum 1000e18 * rewardPerToken / 1e18 always > 0

        uint256 rewardPerToken = duration * rewardRate / totalSupply;
        return balances[account] * rewardPerToken / 1e18;
    }
}
```

## Pattern #4: Flash Deposit/Withdraw Griefing

### VULNERABLE
```solidity
contract VulnerableFlash {
    uint256 public totalSupply;
    uint256 public rewardPerTokenStored;
    mapping(address => uint256) public balances;

    // ISSUE: No time lock or minimum duration
    function deposit(uint256 amount) external {
        updateReward(msg.sender);
        totalSupply += amount;
        balances[msg.sender] += amount;
        token.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) external {
        updateReward(msg.sender);
        totalSupply -= amount;
        balances[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
    }

    // Attack (in one transaction):
    // Initial: 1000 users with 100 tokens each = 100k total
    // 1. Attacker deposits 1M tokens (totalSupply = 1.1M)
    // 2. updateReward() distributes pending rewards based on 1.1M
    // 3. Legitimate users get 100k/1.1M = 9% each instead of 100%
    // 4. Attacker immediately withdraws 1M tokens
    // 5. Attacker diluted 91% of reward distribution
    // 6. Repeat every block
}
```

### FIXED
```solidity
contract FixedFlash {
    uint256 public totalSupply;
    uint256 public constant LOCK_DURATION = 1 days;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public depositTime;

    function deposit(uint256 amount) external {
        updateReward(msg.sender);
        totalSupply += amount;
        balances[msg.sender] += amount;
        depositTime[msg.sender] = block.timestamp;
        token.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) external {
        // Enforce minimum lock duration
        require(
            block.timestamp >= depositTime[msg.sender] + LOCK_DURATION,
            "Tokens locked"
        );

        updateReward(msg.sender);
        totalSupply -= amount;
        balances[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
    }

    // Flash attacks prevented - must lock for 1 day
}
```

## Pattern #5: Update Not Called After Reward Distribution

### VULNERABLE
```solidity
contract VulnerableStaleIndex {
    uint256 public rewardPerTokenStored;
    uint256 public lastUpdateTime;
    uint256 public rewardRate;

    function updateReward(address account) internal {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        // ... update user rewards
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) return rewardPerTokenStored;

        uint256 duration = block.timestamp - lastUpdateTime;
        return rewardPerTokenStored + (duration * rewardRate / totalSupply);
    }

    // ISSUE: notifyRewardAmount doesn't call updateReward
    function notifyRewardAmount(uint256 reward) external {
        rewardRate = reward / DURATION;
        // Missing updateReward()!
        // rewardPerTokenStored not updated
        // Next calculation uses stale lastUpdateTime
    }

    // Result: Rewards double-counted or missed
}
```

### FIXED
```solidity
contract FixedStaleIndex {
    uint256 public rewardPerTokenStored;
    uint256 public lastUpdateTime;
    uint256 public rewardRate;

    function updateReward(address account) internal {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        // ... update user rewards
    }

    function notifyRewardAmount(uint256 reward) external {
        // Update index BEFORE changing rate
        updateReward(address(0));

        rewardRate = reward / DURATION;
        lastUpdateTime = block.timestamp;

        // Index synchronized with new rate
    }
}
```

## Pattern #6: Balance Caching Issues

### VULNERABLE
```solidity
contract VulnerableCache {
    mapping(address => uint256) public balances;
    mapping(address => uint256) public rewards;
    uint256 public rewardPerTokenStored;

    function getReward() external {
        updateReward(msg.sender);

        uint256 reward = rewards[msg.sender];

        // ISSUE: Claiming updates balance incorrectly
        rewards[msg.sender] = 0;
        balances[msg.sender] += reward; // Wrong! Added to stake

        rewardToken.transfer(msg.sender, reward);

        // User's balance now inflated
        // Next earned() calculation uses wrong balance
        // Can claim more than earned
    }

    function earned(address account) public view returns (uint256) {
        // Uses inflated balance from previous claim
        return balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18
            + rewards[account];
    }
}
```

### FIXED
```solidity
contract FixedCache {
    mapping(address => uint256) public balances; // Staked amount only
    mapping(address => uint256) public rewards; // Pending rewards
    uint256 public rewardPerTokenStored;

    function getReward() external {
        updateReward(msg.sender);

        uint256 reward = rewards[msg.sender];
        rewards[msg.sender] = 0;

        // Don't modify balances - only send reward
        rewardToken.transfer(msg.sender, reward);
    }

    function earned(address account) public view returns (uint256) {
        // Uses correct staked balance only
        return balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18
            + rewards[account];
    }
}
```

## Complete Staking Contract Example

### VULNERABLE
```solidity
contract CompleteVulnerable {
    IERC20 public stakingToken; // WETH
    IERC20 public rewardToken; // WETH - same token!
    uint256 public rewardRate = 1e18;

    function deposit(uint256 amount) external {
        // No minimum, allows dust
        // Uses balance not tracked supply
        uint256 totalSupply = stakingToken.balanceOf(address(this));
        totalSupply += amount;

        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) external {
        // No time lock
        stakingToken.transfer(msg.sender, amount);
    }

    function notifyReward(uint256 amount) external {
        // Missing updateReward call
        rewardRate = amount / 7 days;
    }
}
```

### FIXED
```solidity
contract CompleteFixed {
    IERC20 public stakingToken; // WETH
    IERC20 public rewardToken; // USDC - different!

    uint256 public totalSupply; // Tracked separately
    uint256 public rewardRate;
    uint256 public rewardPerTokenStored;
    uint256 public lastUpdateTime;
    uint256 public constant MIN_STAKE = 1000e18;
    uint256 public constant LOCK_DURATION = 1 days;

    mapping(address => uint256) public balances;
    mapping(address => uint256) public depositTime;
    mapping(address => uint256) public rewards;
    mapping(address => uint256) public userRewardPerTokenPaid;

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;

        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    function deposit(uint256 amount) external updateReward(msg.sender) {
        require(
            balances[msg.sender] + amount >= MIN_STAKE,
            "Below minimum"
        );

        totalSupply += amount;
        balances[msg.sender] += amount;
        depositTime[msg.sender] = block.timestamp;

        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) external updateReward(msg.sender) {
        require(
            block.timestamp >= depositTime[msg.sender] + LOCK_DURATION,
            "Locked"
        );

        totalSupply -= amount;
        balances[msg.sender] -= amount;

        stakingToken.transfer(msg.sender, amount);
    }

    function getReward() external updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        rewards[msg.sender] = 0;
        rewardToken.transfer(msg.sender, reward);
    }

    function notifyRewardAmount(uint256 reward) external updateReward(address(0)) {
        rewardRate = reward / 7 days;
        lastUpdateTime = block.timestamp;
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) return rewardPerTokenStored;

        uint256 duration = block.timestamp - lastUpdateTime;
        return rewardPerTokenStored + (duration * rewardRate * 1e18 / totalSupply);
    }

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

## Summary: Key Protections

1. **Different tokens:** Reward token ≠ staking token
2. **Tracked supply:** totalSupply separate from balance
3. **Minimum stake:** Prevent precision loss (e.g., 1000 tokens)
4. **Time lock:** Minimum 1 day lock prevents flash attacks
5. **Update timing:** Call updateReward before reward distribution
6. **Balance integrity:** Don't modify stake balance during claims

## reference.md

# Staking & Reward Vulnerability Patterns

## Pattern #1: Front-Running First Deposit
**Risk:** Attacker front-runs first deposit to become initial staker, then steals initial WETH rewards by sandwiching the deposit
**Detection:** Check if reward token can be same as staking token, or if initial deposit protection exists
**Impact:** First depositor steals all initial rewards meant for protocol, users receive nothing

## Pattern #2: Reward Dilution via Direct Transfer
**Risk:** Sending staking tokens directly to contract increases totalSupply without proper accounting, diluting rewards per share
**Detection:** Verify totalSupply tracks staked amounts separately from actual token balance
**Impact:** Attacker dilutes rewards for legitimate stakers, reducing their yield or stealing rewards

## Pattern #3: Precision Loss in Reward Calculation
**Risk:** Small stake amounts or frequent reward updates cause calculated rewards to round down to zero
**Detection:** Check reward calculation precision, minimum stake requirements, and scaling factors
**Impact:** Users with small stakes receive no rewards despite earning them, accumulated loss over time

## Pattern #4: Flash Deposit/Withdraw Griefing
**Risk:** Large instant deposit followed by immediate withdrawal dilutes rewards for existing stakers without committing capital
**Detection:** Verify time locks, minimum stake duration, or anti-sandwich mechanisms
**Impact:** Attacker repeatedly griefs stakers by diluting rewards in single block, reducing everyone's yield

## Pattern #5: Update Not Called After Reward Distribution
**Risk:** Adding new rewards doesn't update rewardPerToken index, causing stale values in subsequent calculations
**Detection:** Check if updateReward() or equivalent called before/after reward distribution
**Impact:** Rewards calculated incorrectly, users receive wrong amounts (too much or too little)

## Pattern #6: Balance Caching Issues
**Risk:** Claiming rewards updates cached user balance incorrectly, causing subsequent calculations to use stale values
**Detection:** Verify balance tracking during claim operations and state consistency
**Impact:** Users claim incorrect reward amounts, double-claim exploits, or reward loss

## templates

```

```

## templates/report-template.md

# Staking & Reward Security Audit Report

## Executive Summary

**Contract:** [Contract Name]
**Audit Date:** [Date]
**Auditor:** [Name/Team]

**Findings Overview:**
- Critical: X
- High: X
- Medium: X
- Low: X

## Findings

---

### [SEVERITY] Finding #X: [Vulnerability Title]

**Pattern:** #X - [Pattern Name from reference.md]

**Location:** `[contract_name.sol:line_numbers]`

**Description:**

[Detailed explanation of the staking/reward vulnerability]

**Vulnerable Code:**

```solidity
function deposit(uint256 amount) external {
    // Missing: minimum stake, time lock, update call, etc.
    totalSupply += amount;
}
```

**Reward Theft/Dilution Analysis:**

[Analyze exploitation scenario:]
- **Attack vector:** [First deposit, direct transfer, flash action, etc.]
- **Rewards at risk:** [Amount of rewards attacker can steal/dilute]
- **Impact on users:** [% of rewards lost by legitimate stakers]
- **Attack cost:** [Gas, capital, etc.]

**Proof of Concept:**

```solidity
contract StakingExploit {
    VulnerableStaking target;

    function attack() external {
        // 1. Setup: Initial protocol state
        // 2. Exploit: Execute attack (front-run, dilute, etc.)
        // 3. Profit: Extract rewards or grief stakers
    }
}
```

**Attack Flow:**
1. [Initial state - total staked, rewards pending]
2. [Attacker action - deposit, transfer, etc.]
3. [State change - how totalSupply/rewards affected]
4. [Profit extraction or grief impact]
5. [Final state - user losses quantified]

**Impact Analysis:**

**Direct Impact:**
- [Immediate reward theft - e.g., "Attacker steals $X of initial rewards"]
- [User loss - e.g., "Legitimate stakers lose Y% of rewards"]

**Systemic Impact:**
- [Repeated exploitation - e.g., "Every new reward distribution vulnerable"]
- [Economic damage - e.g., "Protocol reward distribution broken"]

**Affected Users:**
- [Quantify impact - e.g., "All stakers during reward period"]

**Remediation:**

```solidity
function deposit(uint256 amount) external updateReward(msg.sender) {
    require(
        balances[msg.sender] + amount >= MIN_STAKE,
        "Below minimum"
    );

    totalSupply += amount;
    balances[msg.sender] += amount;
    depositTime[msg.sender] = block.timestamp;

    stakingToken.transferFrom(msg.sender, address(this), amount);
}
```

**Recommendations:**
1. [Primary fix - e.g., "Use different tokens for staking and rewards"]
2. [Secondary fix - e.g., "Implement minimum stake of 1000 tokens"]
3. [Defense in depth - e.g., "Add 1-day time lock on withdrawals"]

**Gas Impact:** [Estimated additional gas cost]
- Time lock tracking: ~5,000 gas
- Minimum stake check: ~100 gas

---

### [SEVERITY] Finding #X: [Next Vulnerability]

[Repeat above structure for each finding]

---

## Severity Definitions

**Critical:** First depositor can steal all initial rewards, direct transfer dilution enabling reward theft, flash deposit/withdraw draining rewards.

**High:** Precision loss causing rewards to round to zero for legitimate users, stale index after distribution causing incorrect calculations.

**Medium:** Suboptimal reward timing reducing efficiency, griefing via flash actions without direct theft.

**Low:** Gas inefficiencies in calculations, missing events for transparency.

## Recommendations Summary

### Immediate Actions (Critical/High)
1. [List critical fixes]
   - Example: "Change reward token from WETH to different token (USDC)"
   - Example: "Track totalSupply separately from token balance"
   - Example: "Implement minimum stake of 1000 tokens"

### Short-term Improvements (Medium)
1. [List medium-priority enhancements]
   - Example: "Add 1-day time lock on withdrawals"
   - Example: "Call updateReward() before notifyRewardAmount()"

### Long-term Enhancements (Low)
1. [List optimization opportunities]
   - Example: "Optimize gas usage in reward calculations"

## Checklist Results

Based on `checklist.md`:

- [x] **Separate tokens:** Reward token differs from staking token ✓/✗
- [x] **No direct transfer dilution:** totalSupply tracked separately ✓/✗
- [x] **Precision protection:** Minimum stake or scaling implemented ✓/✗
- [x] **Flash protection:** Time locks or minimum duration ✓/✗
- [x] **Index updates:** updateReward called properly ✓/✗
- [x] **Balance integrity:** Cached balances correct during claims ✓/✗

## Token Configuration

- **Staking token:** [Address and symbol]
- **Reward token:** [Address and symbol]
- **Same token?** [Yes ❌ / No ✓]

## Staking Parameters

- **Minimum stake:** [Amount or "None" ❌]
- **Lock duration:** [Duration or "None" ❌]
- **Reward rate:** [Amount per second]
- **Total rewards:** [Amount available]

## Reward Distribution Analysis

### First Deposit Scenario

```
Initial rewards: 1000 tokens
First depositor: 1 wei
Attack profit: 990+ tokens (99%+)
Legitimate users: <10 tokens (<1%)
```

### Direct Transfer Dilution

```
Legitimate stakers: 10,000 tokens staked
Attacker sends: 10,000 tokens directly
Total supply: 20,000 (50% dilution)
Reward loss: 50% for all stakers
```

### Precision Loss Calculation

```
Minimum stake: 10 wei
Total supply: 1,000,000 tokens
Reward per token: 1e18 / 1,000,000e18 = 1e-6
User reward: 10 * 1e-6 / 1e18 = 0 (rounds to zero)
```

### Flash Attack Economics

```
Attacker capital: 1,000,000 tokens
Legitimate stakers: 100,000 tokens total
Attack in single block:
  1. Deposit 1M (total = 1.1M)
  2. Dilute rewards by 91%
  3. Withdraw 1M
  4. Net cost: gas only (~$50)
  5. User loss: 91% of 1 block rewards
  6. Repeatable every block
```

## Testing Recommendations

### Unit Tests
- [ ] First depositor attack prevention
- [ ] Direct transfer dilution protection
- [ ] Precision loss with small stakes
- [ ] Flash deposit/withdraw griefing
- [ ] Update call timing validation
- [ ] Balance caching correctness

### Integration Tests
- [ ] Multi-user reward distribution
- [ ] Reward notification with pending stakes
- [ ] Time lock enforcement
- [ ] Minimum stake validation across operations

### Scenario Tests
- [ ] Initial reward distribution to first stakers
- [ ] High-frequency deposit/withdraw patterns
- [ ] Extreme stake sizes (dust and whale)
- [ ] Reward distribution edge cases (zero stakers, etc.)

## Appendix

### Synthetix-Style Reward Distribution

```solidity
// Standard pattern used by Synthetix and forks
modifier updateReward(address account) {
    rewardPerTokenStored = rewardPerToken();
    lastUpdateTime = lastTimeRewardApplicable();

    if (account != address(0)) {
        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
    }
    _;
}

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

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

### Minimum Stake Calculation

```
Target: Ensure rewards > 1 token per distribution period

Reward rate: R tokens per second
Distribution period: D seconds
Total rewards: R * D
Minimum share: 0.01% (to get >1 token)

Required stake: (R * D / 0.0001) = minimum

Example:
- 1000 tokens over 7 days
- R = 1000 / 604800 = 0.00165 tokens/sec
- Target: >1 token per week
- Min share: 1/1000 = 0.1%
- Min stake: totalSupply * 0.001
- If totalSupply = 1M, min = 1000 tokens
```

### Time Lock Patterns

```solidity
// Pattern 1: Fixed lock from deposit
mapping(address => uint256) public depositTime;

function withdraw() external {
    require(block.timestamp >= depositTime[msg.sender] + LOCK, "Locked");
}

// Pattern 2: Weighted average lock
mapping(address => uint256) public weightedLockTime;

function deposit(uint256 amount) external {
    weightedLockTime[msg.sender] =
        (weightedLockTime[msg.sender] * balances[msg.sender] + block.timestamp * amount)
        / (balances[msg.sender] + amount);
}
```

### Direct Transfer Protection

```solidity
// WRONG: Uses balance
function rewardPerToken() public view returns (uint256) {
    uint256 totalSupply = stakingToken.balanceOf(address(this));
    // Attacker can inflate via direct transfer
}

// CORRECT: Uses tracked supply
uint256 public totalSupply; // State variable

function stake(uint256 amount) external {
    totalSupply += amount; // Only increments through stake
}

function rewardPerToken() public view returns (uint256) {
    // Uses tracked value, immune to direct transfers
    if (totalSupply == 0) return rewardPerTokenStored;
}
```

### Precision Considerations

```
Token decimals: 18
Reward decimals: 18
Scaling factor: 1e18

Calculation:
earned = balance * (rewardPerToken - userPaid) / 1e18

Minimum balance for non-zero rewards:
balance * rewardPerToken / 1e18 >= 1
balance >= 1e18 / rewardPerToken

If rewardPerToken = 1e12 (small rewards):
  Min balance = 1e18 / 1e12 = 1e6 wei = 0.000001 tokens

With MIN_STAKE = 1000e18:
  Always earned >= 1000e18 * rewardPerToken / 1e18
  = 1000 * rewardPerToken
  = significant amount
```

