StakingRewardsV2
StakingRewardsV2 is a multi-pool yield farming contract that distributes $CEO tokens to stakers. It uses MasterChef-style pool management with Synthetix reward accumulator math, enhanced by CeosCompanyNFT tranche-based boost multipliers.
Deployed at: 0x80Da869E7EFcc77044F42D5BB016e9AB3424775D (Base Sepolia)
Key Properties
| Property | Value |
|---|---|
| Reward Token | $CEO (minted on demand) |
| Boost Source | CeosCompanyNFT (tokenId range) |
| Precision Scalar | 1e18 |
| Max Emission Rate | 10 $CEO per second |
| Pool 0 | $RUN staking pool |
Tranche Multiplier
The boost multiplier is determined by the CeosCompanyNFT tokenId range held by the staker. The contract checks all NFTs owned by the user and applies the highest available multiplier:
| CeosCard Tier | Token ID Range | Multiplier |
|---|---|---|
| Black (Tranche 1) | 1 — 1,000 | 5x |
| Gold (Tranche 2) | 1,001 — 4,000 | 3x |
| Silver (Tranche 3) | 4,001 — 10,000 | 2x |
| No CeosCard | — | 1x (no boost) |
The multiplier scales the user’s effective stake weight within the pool. A user staking 1,000 $RUN with a Black CeosCard has an effective weight of 5,000 $RUN.
Structs
struct PoolInfo {
IERC20 stakingToken; // Token deposited by users
uint256 allocPoint; // Pool's share of global emissions
uint256 lastRewardTime; // Last timestamp rewards were calculated
uint256 accCeoPerShare; // Accumulated $CEO per effective share (scaled by 1e18)
uint256 totalEffectiveSupply; // Sum of all users' boosted stakes
}
struct UserInfo {
uint256 amount; // Raw tokens staked
uint256 effectiveAmount; // amount * multiplier
uint256 rewardDebt; // Accumulator debt for pending reward calc
uint256 multiplier; // Current tranche multiplier (1, 2, 3, or 5)
}Interface
User Functions
/// @notice Stake tokens into a pool.
/// @param pid Pool index
/// @param amount Token amount to stake
function stake(uint256 pid, uint256 amount) external;
/// @notice Withdraw staked tokens. Harvests pending rewards automatically.
/// @param pid Pool index
/// @param amount Token amount to withdraw
function withdraw(uint256 pid, uint256 amount) external;
/// @notice Claim pending $CEO rewards without changing stake.
/// @param pid Pool index
function claim(uint256 pid) external;
/// @notice Refresh boost multiplier after acquiring or transferring a CeosCard.
/// Harvests pending rewards, then recalculates effective stake with new multiplier.
/// @param pid Pool index
function refreshBoost(uint256 pid) external;
/// @notice Emergency withdraw without caring about pending rewards.
/// Forfeits all unclaimed $CEO. Use only in emergencies.
/// @param pid Pool index
function emergencyWithdraw(uint256 pid) external;View Functions
/// @notice Get pending $CEO rewards for a user in a specific pool.
/// @param pid Pool index
/// @param account User address
/// @return pending Claimable $CEO amount (18 decimals)
function pendingReward(uint256 pid, address account) external view returns (uint256);
/// @notice Get a user's staking info for a pool.
function getUserInfo(uint256 pid, address account) external view returns (UserInfo memory);
/// @notice Get pool configuration and state.
function getPoolInfo(uint256 pid) external view returns (PoolInfo memory);
/// @notice Total number of staking pools.
function getPoolCount() external view returns (uint256);
/// @notice Get the tranche multiplier for any address based on their CeosCard holdings.
function getTrancheMultiplier(address user) external view returns (uint256);Admin Functions (Owner Only)
/// @notice Add a new staking pool.
/// Duplicate staking tokens are rejected.
/// @param stakingToken ERC-20 token to accept deposits for
/// @param allocPoint Pool's share of global emissions (relative to totalAllocPoint)
function addPool(address stakingToken, uint256 allocPoint) external;
/// @notice Update a pool's allocation points.
/// @param pid Pool index
/// @param allocPoint New allocation points
function setPool(uint256 pid, uint256 allocPoint) external;
/// @notice Update global $CEO emission rate.
/// @param newRate Tokens per second (18 decimals, max 10e18)
function setCeoPerSecond(uint256 newRate) external;Events
event PoolAdded(uint256 indexed pid, address stakingToken, uint256 allocPoint);
event PoolUpdated(uint256 indexed pid, uint256 allocPoint);
event Staked(uint256 indexed pid, address indexed user, uint256 amount, uint256 multiplier);
event Withdrawn(uint256 indexed pid, address indexed user, uint256 amount);
event RewardClaimed(uint256 indexed pid, address indexed user, uint256 amount);
event BoostUpdated(uint256 indexed pid, address indexed user, uint256 newMultiplier);
event EmergencyWithdrawn(uint256 indexed pid, address indexed user, uint256 amount);
event CeoPerSecondUpdated(uint256 oldRate, uint256 newRate);Errors
error ZeroAddress(); // Invalid token or NFT address
error ZeroAmount(); // Deposit/withdraw of 0
error PoolNotFound(); // pid >= pool count
error PoolAlreadyExists(); // Duplicate staking token
error InsufficientStake(); // Withdrawing more than staked
error NothingToClaim(); // Zero pending rewards
error ExceedsMaxEmissionRate(); // ceoPerSecond > 10e18Reward Math
The accumulator model calculates rewards without looping over users:
poolReward = elapsed * ceoPerSecond * pool.allocPoint / totalAllocPoint
accCeoPerShare += poolReward * 1e18 / pool.totalEffectiveSupply
pendingReward = user.effectiveAmount * accCeoPerShare / 1e18 - user.rewardDebtWhen a user stakes, withdraws, or refreshes their boost, rewardDebt is recalculated:
rewardDebt = effectiveAmount * accCeoPerShare / 1e18This ensures each user’s pending reward is exactly what accrued during their time in the pool.
Staking Flow
- Owner creates the $RUN pool:
addPool(runTokenAddress, 1000). - User approves $RUN spend:
runToken.approve(stakingRewardsV2, amount). - User calls
stake(0, amount). Contract checks CeosCard for multiplier. - With a Black CeosCard (5x), effective stake =
amount * 5. - $CEO accrues proportional to effective stake weight.
- User calls
claim(0)to mint pending $CEO. - If user acquires a better CeosCard later,
refreshBoost(0)updates the multiplier.
Integration
The StakingRewardsV2 must hold MINTER_ROLE on the CeoToken contract to mint $CEO rewards on demand. The contract queries CeosCompanyNFT via balanceOf and tokenOfOwnerByIndex to determine tranche multipliers. These calls use try/catch to gracefully handle any NFT contract errors without bricking the staking flow.
Security
ReentrancyGuardon all user-facing functions (stake,withdraw,claim,refreshBoost,emergencyWithdraw)SafeERC20for all staking token transfersMAX_CEO_PER_SECONDcap (10e18) prevents emission rate abuse- Duplicate pool prevention via
_poolExistsmapping emergencyWithdrawas a safety valve that forfeits rewards but recovers principaltry/catchon NFT queries prevents external contract failures from blocking withdrawals