Skip to main content

liquidity-mining

Use when implementing liquidity mining programs, LP incentive systems, or MasterChef-style reward distribution. Covers emission schedules, gauge systems, boost mechanics, and reward claiming.

Jump to install

Source facts

Repository
ccashwell/evm-cortex
Last source activity
April 10, 2026 at 16:31
Detected SKILL.md language
English
Stars
131
Forks
18

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
liquidity-mining
description
Use when implementing liquidity mining programs, LP incentive systems, or MasterChef-style reward distribution. Covers emission schedules, gauge systems, boost mechanics, and reward claiming.
# Liquidity Mining Patterns ## MasterChef Architecture The MasterChef pattern distributes a fixed emission rate across multiple pools, weighted by allocation points. Each pool accrues rewards to its stakers using the Synthetix per-token accumulator. ``` Pool reward rate = totalEmission * pool.allocPoint / totalAllocPoint User reward = user.amount * (pool.accRewardPerShare - user.rewardDebt) ``` ## MasterChef-Style Implementation ```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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract MasterChef is Ownable { using SafeERC20 for IERC20; struct UserInfo { uint256 amount; // LP tokens staked uint256 rewardDebt; // subtracted from accumulated reward } struct PoolInfo { IERC20 lpToken; uint256 allocPoint; uint256 lastRewardTime; uint256 accRewardPerShare; // scaled by 1e12 } IERC20 public immutable rewardToken; uint256 public rewardPerSecond; uint256 public totalAllocPoint; PoolInfo[] public poolInfo; mapping(uint256 => mapping(address => UserInfo)) public userInfo; constructor(IERC20 _rewardToken, uint256 _rewardPerSecond) Ownable(msg.sender) { rewardToken = _rewardToken; rewardPerSecond = _rewardPerSecond; } function poolLength() external view returns (uint256) { return poolInfo.length; } function addPool(uint256 _allocPoint, IERC20 _lpToken) external onlyOwner { totalAllocPoint += _allocPoint; poolInfo.push(PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardTime: block.timestamp, accRewardPerShare: 0 })); } function setPool(uint256 _pid, uint256 _allocPoint) external onlyOwner { totalAllocPoint = totalAllocPoint - poolInfo[_pid].allocPoint + _allocPoint; poolInfo[_pid].allocPoint = _allocPoint; } function pendingReward(uint256 _pid, address _user) external view returns (uint256) { PoolInfo memory pool = poolInfo[_pid]; UserInfo memory user = userInfo[_pid][_user]; uint256 accRewardPerShare = pool.accRewardPerShare; uint256 lpSupply = pool.lpToken.balanceOf(address(this)); if (block.timestamp > pool.lastRewardTime && lpSupply > 0) { uint256 elapsed = block.timestamp - pool.lastRewardTime; uint256 reward = elapsed * rewardPerSecond * pool.allocPoint / totalAllocPoint; accRewardPerShare += reward * 1e12 / lpSupply; } return user.amount * accRewardPerShare / 1e12 - user.rewardDebt; } function updatePool(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; if (block.timestamp <= pool.lastRewardTime) return; uint256 lpSupply = pool.lpToken.balanceOf(address(this)); if (lpSupply == 0) { pool.lastRewardTime = block.timestamp; return; } uint256 elapsed = block.timestamp - pool.lastRewardTime; uint256 reward = elapsed * rewardPerSecond * pool.allocPoint / totalAllocPoint; pool.accRewardPerShare += reward * 1e12 / lpSupply; pool.lastRewardTime = block.timestamp; } function deposit(uint256 _pid, uint256 _amount) external { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; updatePool(_pid); if (user.amount > 0) { uint256 pending = user.amount * pool.accRewardPerShare / 1e12 - user.rewardDebt; if (pending > 0) { rewardToken.safeTransfer(msg.sender, pending); } } if (_amount > 0) { pool.lpToken.safeTransferFrom(msg.sender, address(this), _amount); user.amount += _amount; } user.rewardDebt = user.amount * pool.accRewardPerShare / 1e12; emit Deposit(msg.sender, _pid, _amount); } function withdraw(uint256 _pid, uint256 _amount) external { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; require(user.amount >= _amount, "insufficient balance"); updatePool(_pid); uint256 pending = user.amount * pool.accRewardPerShare / 1e12 - user.rewardDebt; if (pending > 0) { rewardToken.safeTransfer(msg.sender, pending); } if (_amount > 0) { user.amount -= _amount; pool.lpToken.safeTransfer(msg.sender, _amount); } user.rewardDebt = user.amount * pool.accRewardPerShare / 1e12; emit Withdraw(msg.sender, _pid, _amount); } function emergencyWithdraw(uint256 _pid) external { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; uint256 amount = user.amount; user.amount = 0; user.rewardDebt = 0; pool.lpToken.safeTransfer(msg.sender, amount); emit EmergencyWithdraw(msg.sender, _pid, amount); } event Deposit(address indexed user, uint256 indexed pid, uint256 amount); event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); } ``` ## Emission Schedule Patterns ```solidity // Halving schedule (like BTC) function getRewardPerSecond() public view returns (uint256) { uint256 epoch = (block.timestamp - startTime) / EPOCH_DURATION; return initialRate >> epoch; // halve each epoch } // Linear decay function getRewardPerSecond() public view returns (uint256) { uint256 elapsed = block.timestamp - startTime; if (elapsed >= totalDuration) return 0; return initialRate * (totalDuration - elapsed) / totalDuration; } ``` ## Gauge-Based Allocation Instead of owner setting allocPoints, let token holders vote on pool weights: ```solidity mapping(uint256 => uint256) public poolVotes; // pid -> total votes mapping(address => mapping(uint256 => uint256)) public userVotes; function vote(uint256 _pid, uint256 _weight) external { uint256 votingPower = veToken.balanceOf(msg.sender); require(_weight <= votingPower - usedVotes[msg.sender], "exceeded voting power"); poolVotes[_pid] += _weight; userVotes[msg.sender][_pid] += _weight; usedVotes[msg.sender] += _weight; // allocPoints recalculated from poolVotes at epoch boundaries } ``` ## Checklist - [ ] `updatePool()` called before any deposit/withdraw - [ ] `rewardDebt` updated after every balance change - [ ] `emergencyWithdraw` skips reward calculation (safety valve) - [ ] Pool allocation changes via `massUpdatePools()` to prevent stale accrual - [ ] Emission rate accounts for actual reward token balance - [ ] No duplicate LP token addresses across pools - [ ] Test reward distribution across multiple users and pools - [ ] Verify no reward loss from rounding in `accRewardPerShare`
View on GitHub