Skip to Content
Smart ContractsStakingRewardsV2

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

PropertyValue
Reward Token$CEO (minted on demand)
Boost SourceCeosCompanyNFT (tokenId range)
Precision Scalar1e18
Max Emission Rate10 $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 TierToken ID RangeMultiplier
Black (Tranche 1)1 — 1,0005x
Gold (Tranche 2)1,001 — 4,0003x
Silver (Tranche 3)4,001 — 10,0002x
No CeosCard1x (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 > 10e18

Reward 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.rewardDebt

When a user stakes, withdraws, or refreshes their boost, rewardDebt is recalculated:

rewardDebt = effectiveAmount * accCeoPerShare / 1e18

This ensures each user’s pending reward is exactly what accrued during their time in the pool.

Staking Flow

  1. Owner creates the $RUN pool: addPool(runTokenAddress, 1000).
  2. User approves $RUN spend: runToken.approve(stakingRewardsV2, amount).
  3. User calls stake(0, amount). Contract checks CeosCard for multiplier.
  4. With a Black CeosCard (5x), effective stake = amount * 5.
  5. $CEO accrues proportional to effective stake weight.
  6. User calls claim(0) to mint pending $CEO.
  7. 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

  • ReentrancyGuard on all user-facing functions (stake, withdraw, claim, refreshBoost, emergencyWithdraw)
  • SafeERC20 for all staking token transfers
  • MAX_CEO_PER_SECOND cap (10e18) prevents emission rate abuse
  • Duplicate pool prevention via _poolExists mapping
  • emergencyWithdraw as a safety valve that forfeits rewards but recovers principal
  • try/catch on NFT queries prevents external contract failures from blocking withdrawals