Catalog / module / rewards
Staking vault
v1Stake your NFT, earn rewards.
What it does
staking-vault v1: Stake your NFT, earn rewards
Holders stake an NFT from one collection and earn one or more ERC-20 reward tokens. There is one pool per reward token, and every staked NFT earns from every pool in proportion to its **weight**. Without a plugin the weight is 1.00x; a weight plugin (`duration-weight`, `task-weight` or `composite-weight`) can raise it.
Ported from ENTITY's `MaterialHouses` (accumulator, drip, clamped level source). It adds the ParkingVault pull adapter and the cross-review fixes.
How it pays
- **Drip, not dump.** A deposit is added to the pool's *undripped* balance, which is released linearly until `dripEnd`. While nothing is staked, no time is released and the window is pushed forward. A first staker therefore buys the drip *rate*, never the balance. - **No grief stretching.** A deposit restarts the window only if the depositor is an approved **funder** or brings at least `minFundBps` of the undripped balance. When the old window has already ended, any deposit opens a new one, so deposits never stall. - **Weight.** Weight is `1,000,000 × plugin multiplier`, clamped to `[1.00x, maxMultiplierBps]`. The plugin is called with a 150k gas budget. A revert, a short return, garbage or running out of gas all read as 1.00x. If the *caller* forwards too little gas for the full budget, the call reverts (`PluginGasTooLow`) instead of silently reading 1.00x, so nobody can grief another holder's weight through `sync`. - **Weights change only through `sync` / `syncMany`.** Both are permissionless. They settle everything earned so far at the old weight before switching, so a new weight never reaches backwards. The site calls `syncMany` before every claim. - **Custody is exact.** For each reward token, `custody == net funded − paid out == token balance` (when nobody sends tokens directly). Undripped, released-but-unclaimed and owed rewards all sit inside custody.
Roles
| Who | Can | |---|---| | Owner | `addPool` (append-only, ≤ 8, never replaces a pool); `setParams` (drip window 1h..30d, min stake 0..7d, wallet limit, minFund 0..100%); `setFunder`; `setRewardSource`; `pause`/`unpause` (entries only); `rescue` (only balance above custody); `rescueNft` (never a staked id); ownership handover (2-step) | | Funder (`isFunder`) | deposits always restart the drip window. Recipes grant this to the predicted upstream address (splitter, mint) | | Anyone | `fund`, `pull`, `sync`, `syncMany` | | Staker | `stake`, `stakeMany`, `claim`, `claimAll`, `unstake`, `emergencyUnstake` for their own NFTs |
**Locked forever:** `collection`, `weightPlugin`, `maxMultiplierBps`.
Entries and exits
- **Entries** (closed while paused, halted or migrated): `stake`, `stakeMany`, `fund`, `pull`. - **Exits** (always open, and they never call the plugin): `claim`, `claimAll` (skips NFTs still inside minStake), `unstake`, `emergencyUnstake`. `emergencyUnstake` ignores minStake, pays nothing, and returns that NFT's pending rewards to the pools' drip for the remaining stakers. - `minStake` gates both claim and unstake, so nobody can stake, claim and leave in one block. A later *raise* never extends an existing stake's lock; a *cut* shortens it.
Value flows and fee lanes
| Path | Flow | Notes | |---|---|---| | `fund(poolId, amount)` | `FundRewards` | `_pullIn`, fee on what actually arrived (fee-on-transfer safe). `payable` to match `IBlocksmithFundable`, but any ETH is refused (wrap to WETH first). | | `pull(poolId)` | `FundRewards` | `IRewardSource.pull()`, measured by balance delta, charged via `_chargeHeld` | | stake / unstake | none | NFTs are free principal (`_creditUncharged` / `_consume`). The vault is **never** flagged `FLAG_CHARGES_INFLOW` | | claims / unstakes | none | `_pushOut`, never charged |
A flagged instance (for example FeeSplitter or BurnMintNFT) that funds via `forceApprove` + `fund` is exempt, so value is charged exactly once.
Caps
- Reward custody per token ≤ the tier's `maxSupplyBps` of `min(snapshot, totalSupply)` (Reviewed 10%). For WETH on 4663 (~41.4k supply) that is about 4.1k WETH per vault. - NFT custody ≤ `maxNftBps` of the collection's supply (Tested 25%, Reviewed 100%).
Views (no eth_getLogs needed)
`poolTotals()` (every pool: token, drip state, accumulator, funded, paid, forfeited), `pending(id)`, `stakeBatch(from, count ≤ 500)`, `stakedBy(owner)`, `stakeInfo(id)`, `stakeOwner(id)`, `unlocksAt(id)`, `multiplierBpsOf(id)`, `earnedFrom(id, pool)`, `counters()`, `params()`, `funders()`, plus the base `history` / `stats`.
Implementation note
The accounting lives in the linked library `StakingVaultEngine` (public functions run in the vault's context) to stay under EIP-170. The library never moves value. Deploy and link the library before the implementation; the implementation's codehash includes the library address.
Risks in plain English
- Rewards are one way. Once deposited they belong to stakers. - A reward token that blocks transfers blocks `claim`/`unstake` for that token. `emergencyUnstake` still returns the NFT. - The weight plugin decides relative shares. Pick one you trust; it can never be swapped. - Tokens sent straight to the vault are not distributed; only the owner can return them (`rescue`, excess above custody only).
What if…
- **…I lose my wallet?** The stake belongs to the staking wallet. Nobody can move it for you. - **…the owner pauses or the module is halted?** Only new stakes and deposits stop. Claims, unstakes and emergency unstakes keep working, also after a migration. - **…someone deposits 1 wei every block to delay payouts?** Nothing changes. Only funders or meaningful deposits restart the window. - **…a big deposit lands while the vault is empty?** The window waits. The first staker earns at the drip rate, not the lump. - **…the plugin reverts or burns gas?** That NFT counts as 1.00x; staking and exits are unaffected.
Risks in plain English
- Rewards are one way: once deposited they belong to stakers, and no key can take them back out.
- Payouts are only as good as the reward tokens. If a reward token blocks transfers, use emergency unstake to get the NFT back.
- The weight plugin decides relative shares. A weight change only applies after a sync, and never to rewards already earned.
- The owner can change the drip window, minimum stake (raises apply to new stakes only), wallet limit, funders and reward sources, and add pools. The owner cannot touch staked NFTs or reward custody.
- Tokens sent straight to the vault (not via Add rewards) are not paid out to stakers; only the owner can send them back out (rescue never touches reward custody).
- Caps: the vault holds at most a set share of each reward token's supply (10% at Reviewed) and of the collection (25% Tested, 100% Reviewed).
What if…
- What if I lose my wallet?
- Your stake belongs to the wallet that staked it. Nobody, including the owner, can move it for you, so keep that wallet safe.
- What if the owner pauses the vault?
- Only new stakes and deposits stop. You can still claim, unstake and emergency unstake at any time, also when the vault is halted or migrated.
- What if someone deposits 1 wei over and over to slow payouts?
- It does nothing. Only an approved funder, or a deposit of at least the minimum share of what is still dripping, restarts the payout window.
- What if a big deposit lands while nobody is staked?
- No time is released while the vault is empty; the window just waits. The first staker earns at the drip rate, not the whole deposit.
- What if the weight plugin breaks?
- The vault reads it with a fixed gas budget. If it reverts or misbehaves you are counted at 1.00x; staking and every exit keep working.
Fees
| Lane | Fee |
|---|---|
| Reward fundingwhen anyone adds rewards (fund), or rewards are collected from a reward source (pull) | 0.5% |
Fees are fixed into each contract at deploy and can never change for it. Claims, withdrawals and refunds are never charged. Hard cap 1%, written into the code.
Evidence
- Form check
- Built from the source files
- Implementation
- —
- Code hash
- —
Settings you choose at deploy
- Your NFT collectionThe ERC-721 holders stake. Can never change.Locked forever
- Reward tokensOne pool per token. Every staked NFT earns from every pool. More can be added later (up to 8).Changeable
- Weight pluginOptional: a duration, task or composite weight module. Empty = everyone earns at 1.00x. Can never change.Locked forever
- Highest multiplier the vault will honourCaps whatever the plugin reports. 30000 = 3.00x. Can never change.Locked forever
- Spread each reward deposit overLonger = smoother payouts and no first-staker windfall.Changeable
- Minimum time staked before claiming or unstakingChangeable
- Most NFTs one wallet can stake at once0 = unlimited.Changeable
- Smallest deposit that restarts the payout windowAs a share of the rewards still dripping. Approved funders always restart it.Changeable
- Approved fundersKeepers or upstream modules (fee splitter, NFT mint) whose deposits always restart the window.Changeable