Staking pools
A per-token pool that pays two reward assets — the token side of LP fees and the creator WETH share.
If a launch picks the staking fee policy on either side, the module clones a TokenStakingPool dedicated to that token. Holders stake the token and earn from two independent reward streams: the token side of LP fees, and the creator’s WETH share. Pick staking on both sides and one pool pays both.
#The rules
deposit(amount) after an ERC-20 approve.claimRewards() pays both assets at once.#How rewards accrue
Each asset has its own rewardPerShare accumulator, so the two streams are completely independent — a token that only picked staking on the WETH side simply never accrues the token stream. Rewards are distributed pro-rata across whoever is staked at the moment they arrive; there is no vesting, no boost curve, and no lock multiplier.
- Rewards that arrive at an empty pool are queued, not lost. They sit in
queuedand are released into the accumulator on the first deposit — and that depositor shares in them, since debts are snapshotted before the release. - Anyone can top the pool up.
donateRewards(the token),donateWethRewards(WETH) anddonateEthRewards(native ETH, wrapped on arrival) are all open. A creator running a campaign, or a community pooling for one, needs no permission. - Only the fee module can call the notify hooks. Its sole power is distributing rewards that have already been transferred in — it cannot move stakes and cannot take anything out.
#Pool API
// staking
function deposit(uint256 amount) external; // re-locks your whole stake 24h
function withdraw(uint256 amount) external; // reverts StillLocked(lockedUntil)
function claimRewards() external returns (uint256 tokenAmount, uint256 wethAmount);
function exit() external; // withdraw everything + claim
// top-ups — open to anyone
function donateRewards(uint256 amount) external; // in the staked token
function donateWethRewards(uint256 amount) external; // in WETH
function donateEthRewards() external payable; // in native ETH, wrapped here
// reads
function earned(address user) external view returns (uint256 tokenAmount, uint256 wethAmount);
function getStake(address user) external view returns (StakeInfo memory);
function totalStaked() external view returns (uint256);
function tokenRewards() external view returns (uint256 accPerShare, uint256 queued,
uint256 totalAdded, uint256 totalPaid);
function wethRewards() external view returns (uint256 accPerShare, uint256 queued,
uint256 totalAdded, uint256 totalPaid);
uint64 public constant LOCK_DURATION = 1 days;#Finding a token’s pool
Pools are EIP-1167 clones, one per token, created at launch. Resolve one with StakingFeeModule.poolOf(token). The indexed StakingPool entity carries the same pool plus the staked total and both reward counters (Data & indexing). A zero address means the token did not choose staking — pools cannot be added afterwards.
#Reading the numbers honestly
| Field | Means |
|---|---|
| totalAdded | Lifetime rewards this pool has received in that asset. |
| totalPaid | Lifetime rewards claimed out of it. |
| queued | Received while nobody was staked; releases at the next deposit. |
| accPerShare | The accumulator, scaled by 1e18. Not a rate — do not read it as APR. |
#Upgrades
StakingFeeModule.setPoolImplementation swaps the master copy that future launches clone. Existing pools are standalone clones of whatever implementation was current when they were created and are never touched by that call — including their code. The master copy itself is bricked in its constructor, so nobody can initialise it.
