Skip to main content

staking-reward-patterns

Use when implementing staking contracts, reward distribution systems, or yield farming. Covers the Synthetix reward model, per-second accrual, cooldown periods, and boosted reward mechanics.

소스 정보

저장소
ccashwell/evm-cortex
최근 소스 활동
2026년 4월 10일 16:31
감지된 SKILL.md 언어
영어
스타
131
포크
18

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
staking-reward-patterns
description
Use when implementing staking contracts, reward distribution systems, or yield farming. Covers the Synthetix reward model, per-second accrual, cooldown periods, and boosted reward mechanics.
# Staking & Reward Distribution Patterns ## Synthetix Reward Model The industry-standard approach for distributing rewards proportionally to stakers without iterating over all stakers. Gas cost is O(1) per user action. **Core formula**: ``` rewardPerToken = rewardPerToken + (elapsed * rewardRate / totalStaked) earned(user) = balance(user) * (rewardPerToken - userRewardPerTokenPaid(user)) + rewards(user) ``` ## Synthetix-Style Reward Contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract StakingRewards is ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable stakingToken; IERC20 public immutable rewardToken; address public rewardDistributor; uint256 public rewardRate; // rewards per second uint256 public periodFinish; // when current reward period ends uint256 public lastUpdateTime; uint256 public rewardPerTokenStored; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => uint256) public userRewardPerTokenPaid; mapping(address => uint256) public rewards; uint256 public constant DURATION = 7 days; constructor(address _stakingToken, address _rewardToken, address _distributor) { stakingToken = IERC20(_stakingToken); rewardToken = IERC20(_rewardToken); rewardDistributor = _distributor; } modifier updateReward(address account) { rewardPerTokenStored = rewardPerToken(); lastUpdateTime = lastTimeRewardApplicable(); if (account != address(0)) { rewards[account] = earned(account); userRewardPerTokenPaid[account] = rewardPerTokenStored; } _; } function lastTimeRewardApplicable() public view returns (uint256) { return block.timestamp < periodFinish ? block.timestamp : periodFinish; } 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 ( balanceOf[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18 ) + rewards[account]; } function stake(uint256 amount) external nonReentrant updateReward(msg.sender) { require(amount > 0, "Cannot stake 0"); totalSupply += amount; balanceOf[msg.sender] += amount; stakingToken.safeTransferFrom(msg.sender, address(this), amount); emit Staked(msg.sender, amount); } function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) { require(amount > 0, "Cannot withdraw 0"); totalSupply -= amount; balanceOf[msg.sender] -= amount; stakingToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } function claim() external nonReentrant updateReward(msg.sender) { uint256 reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; rewardToken.safeTransfer(msg.sender, reward); emit RewardPaid(msg.sender, reward); } } function exit() external { withdraw(balanceOf[msg.sender]); claim(); } function notifyRewardAmount(uint256 reward) external updateReward(address(0)) { require(msg.sender == rewardDistributor, "unauthorized"); if (block.timestamp >= periodFinish) { rewardRate = reward / DURATION; } else { uint256 remaining = periodFinish - block.timestamp; uint256 leftover = remaining * rewardRate; rewardRate = (reward + leftover) / DURATION; } require(rewardRate > 0, "reward rate = 0"); require( rewardRate * DURATION <= rewardToken.balanceOf(address(this)), "reward amount > balance" ); lastUpdateTime = block.timestamp; periodFinish = block.timestamp + DURATION; emit RewardAdded(reward); } event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardPaid(address indexed user, uint256 reward); event RewardAdded(uint256 reward); } ``` ## Cooldown Period Pattern ```solidity uint256 public constant COOLDOWN_DURATION = 10 days; uint256 public constant UNSTAKE_WINDOW = 2 days; mapping(address => uint256) public cooldownStart; function startCooldown() external { require(balanceOf[msg.sender] > 0, "nothing staked"); cooldownStart[msg.sender] = block.timestamp; } function withdraw(uint256 amount) external { uint256 cooldown = cooldownStart[msg.sender]; require(cooldown > 0, "cooldown not started"); require(block.timestamp >= cooldown + COOLDOWN_DURATION, "cooldown active"); require( block.timestamp <= cooldown + COOLDOWN_DURATION + UNSTAKE_WINDOW, "unstake window closed" ); cooldownStart[msg.sender] = 0; // ... transfer logic } ``` ## Reward Boosting (ve-Style) ```solidity // Boost based on lock duration: longer lock = higher multiplier function getBoost(address user) public view returns (uint256) { uint256 lockEnd = lockEndTime[user]; if (lockEnd <= block.timestamp) return 1e18; // 1x (no boost) uint256 remaining = lockEnd - block.timestamp; uint256 maxDuration = 4 * 365 days; // Linear boost: 1x to 2.5x based on lock duration return 1e18 + (remaining * 15e17 / maxDuration); } function earned(address account) public view returns (uint256) { uint256 base = balanceOf[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18; return (base * getBoost(account) / 1e18) + rewards[account]; } ``` ## Checklist - [ ] Use `updateReward` modifier on every state-changing function - [ ] `rewardPerToken()` handles `totalSupply == 0` (avoid division by zero) - [ ] `notifyRewardAmount` checks sufficient reward token balance - [ ] Reward rate calculation handles mid-period top-ups correctly - [ ] Apply `ReentrancyGuard` on stake/withdraw/claim - [ ] Use `SafeERC20` for all token transfers - [ ] Consider cooldown period for protocol safety - [ ] Test reward accrual across multiple stakers and time periods - [ ] Verify no reward dust is lost due to integer division
GitHub에서 보기