An epoch-based staking vault that modifies the MasterChef/Synthetix reward distribution algorithm to operate on epoch cycles rather than per-block distribution.
This implementation diverges from traditional MasterChef contracts by introducing epoch-based reward distribution. While MasterChef distributes rewards every block based on continuous time-weighted stakes, this system accumulates rewards during lock periods and distributes them based on epoch participation.
Traditional MasterChef:
- Distributes rewards every block
- Continuous reward accrual
- Immediate liquidity
This Implementation:
- Distributes rewards per epoch cycle
- Rewards only accrue during lock periods
- Structured liquidity windows
βββββββββββββββββββββββββββ
β StableCoinRewardsVault β β Epoch-based reward distribution
βββββββββββββββββββββββββββ€
β EpochStakingVault β β Epoch lifecycle management
βββββββββββββββββββββββββββ€
β ERC4626 β β Tokenized vault standard
β AccessControl β β Role-based permissions
β ReentrancyGuard β β Security layer
βββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EPOCH N TIMELINE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DEPOSIT WINDOW (7 days) β LOCK PERIOD (90 days) β
βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β β’ Deposits/Withdrawals: OPEN β β’ Deposits/Withdrawals: CLOSED β
β β’ Reward Addition: BLOCKED β β’ Reward Addition: ALLOWED β
β β’ Parameters: Updatable (24h) β β’ Parameters: LOCKED β
β β’ Rewards: NOT CLAIMABLE β β’ Rewards: CLAIMABLE β
βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
β
Automatic transition to Epoch N+1
The core innovation is the dual accumulator system that enables epoch-based distribution:
// Two accumulators instead of one
uint256 public totalRewardsPerShareAccumulator; // Tracks all rewards added
uint256 public claimableRewardsPerShareAccumulator; // Tracks claimable rewards
// Synchronization function - the key difference from MasterChef
function syncToCurrentEpoch() internal {
if (
(block.timestamp < startTime + DEPOSIT_WINDOW ||
block.timestamp > startTime + DEPOSIT_WINDOW + LOCK_PERIOD) &&
totalRewardsPerShareAccumulator != claimableRewardsPerShareAccumulator
) {
// Makes accumulated rewards claimable outside deposit window
claimableRewardsPerShareAccumulator = totalRewardsPerShareAccumulator;
}
}MasterChef Pattern:
// Rewards distributed per block
rewardPerBlock = totalRewards / numberOfBlocks
pendingReward = (block.number - lastRewardBlock) * rewardPerBlock * userShare / totalSharesThis Implementation:
// Rewards added during lock period, distributed based on epoch participation
rewardPerShare += (addedRewards * 1e27) / totalSupply
userRewards = shares * (claimableRewardsPerShare - userDebtPerShare) / 1e27Block Timeline:
ββ Block N: User deposits during window
β ββ userDebt = currentAccumulator (prevents claiming existing rewards)
β
ββ Block N+100: Deposit window closes
β ββ No state change yet
β
ββ Block N+101: Manager adds rewards
β ββ totalRewardsPerShareAccumulator increases
β ββ claimableRewardsPerShareAccumulator unchanged (rewards locked)
β
ββ Block N+200: User interacts (outside deposit window)
β ββ syncToCurrentEpoch() called
β ββ claimableRewardsPerShareAccumulator = totalRewardsPerShareAccumulator
β ββ User can now claim rewards
Uses 1e27 scaling factor to prevent precision loss:
uint256 rewardPerShare = amount.mulDiv(1e27, totalSupply, Math.Rounding.Floor);
if (rewardPerShare == 0) revert RewardAmountTooLowComparedToTotalSupply();The updateReward modifier automatically processes rewards on user interactions:
modifier updateReward(address user) {
syncToCurrentEpoch(); // Critical: Check if we should unlock rewards
UserInfo storage _user = userInfo[user];
uint256 rewards = claimableRewards(user);
_user.rewardsPerShareDebt = claimableRewardsPerShareAccumulator;
if (rewards > 0) {
_user.totalRewardsClaimed += rewards;
REWARD_TOKEN.safeTransfer(user, rewards);
emit RewardsClaimed(user, rewards);
}
_;
}Prevents operations during wrong epoch phases:
modifier isOpen() {
if (block.timestamp >= startTime + DEPOSIT_WINDOW &&
block.timestamp < startTime + DEPOSIT_WINDOW + LOCK_PERIOD) {
revert EpochLocked();
}
_;
}
modifier isLocked() {
if (block.timestamp < startTime + DEPOSIT_WINDOW ||
block.timestamp >= startTime + DEPOSIT_WINDOW + LOCK_PERIOD) {
revert NotLocked();
}
_;
}- Addressed: Parameter updates restricted to first 24h
- Addressed: Deposits/withdrawals blocked during reward distribution
- Addressed: Epoch transitions require explicit manager action
- Min/Max Limits: Prevent dust attacks and whale domination
- Pool Size Cap: Limits total exposure
- Reward Validation: Ensures meaningful reward distribution
// Deploy with epoch-specific parameters
const vault = await StableCoinRewardsVault.deploy(
"0x...", // USDC or other stable asset
"Epoch USDC", // Clear naming convention
"epUSDC", // Indicates epoch-based
admin, // Multisig recommended
manager, // Can be automated keeper
100e6, // $100 minimum (USDC decimals)
10000e6, // $10k maximum
1000000e6 // $1M pool cap
);
// Start first epoch
await vault.connect(manager).startEpoch();Critical test scenarios for epoch-based distribution:
- Epoch boundary conditions
- Reward accumulation across multiple epochs
- User joining mid-epoch
- Multiple reward additions in single epoch
- Gas optimization verification
- Dynamic Epochs: Variable duration based on TVL
- Multi-Asset Rewards: Distribute multiple tokens
- Boost Mechanism: NFT or veToken multipliers
- Cross-Epoch Strategies: Compound rewards automatically
Note: This implementation significantly modifies the standard MasterChef algorithm. Ensure thorough understanding of epoch mechanics before deployment.