LP Staking Vault (LpStakingVault7D)
LpStakingVault7D lets users stake Aerodrome WETH/CLAIM LP tokens and earn CLAIM rewards. Rewards flow from the Furnace (LP top-up split + overflow drip), sellback LP cuts, and the vault’s own Aerodrome fee harvest. This is the protocol’s incentive layer for liquidity providers.
User lifecycle (ABI surface)
Stake:
stake(amount)- bonds LP immediately
- updates reward accounting
Unbond (start cooldown):
beginUnbond(amount)- removes LP from
stakedBalanceimmediately - creates an onchain unbond entry (
id,amount,unlockTime)
- removes LP from
- Constants (v1.0.0):
UNBONDING_PERIOD = 7 daysMAX_UNBONDS_PER_USER = 25(hard cap on active entries)
Withdraw:
withdrawMatured()- withdraws all matured unbond entries
- bounded in practice by
MAX_UNBONDS_PER_USER
Views (for UIs):
getUnbondCount(user)getUnbondByIndex(user, index) -> (unbondId, amount, unlockTime)- swap-and-pop removes withdrawn entries, so only active entries are returned
Rewards (CLAIM):
claimRewards()— Harvest (liquid CLAIM to wallet)claimRewardsAndLock(targetTokenId, durationSeconds, createAutoMax, minVeOut)— Harvest & Lock- routes through Furnace so the Furnace bonus applies
- caller supplies
minVeOut(derive from Furnace quote the same way as a normal Furnace entry) - the emitted
LpRewardsLocked.tokenIdis the actual destination lock id returned by Furnace; if a new lock is created, index the minted token id rather than assuming0
Reward sources
Rewards credited by the vault can come from:
- Furnace-funded LP rewards (top-ups + streamed drip)
- Furnace sellback cut routed to LP rewards
- the vault’s own Aerodrome LP fee harvest (WETH/CLAIM fees -> CLAIM rewards)
Reward accounting uses a standard reward-per-token accumulator:
ACC = 1e18
Fee harvest: Aerodrome LP fees -> CLAIM rewards
Owner-or-keeper-allowlisted method (vault owner or allowlisted harvest keeper):
harvestFeesToRewards(deadline, minClaimOut)- first checkpoints any pre-existing unaccounted CLAIM (for example from a prior swallowed Furnace
notifyRewards(...)failure) so this call’sfeeClaim/claimToRewardsreflect the current harvest only - calls Aerodrome pool
claimFees()(WETH + CLAIM) - pays an optional WETH bounty (staleness-gated)
- swaps remaining WETH -> CLAIM
- credits (fee CLAIM + bought CLAIM) as new rewards using balance-delta accounting
- first checkpoints any pre-existing unaccounted CLAIM (for example from a prior swallowed Furnace
Preview helper (UI / keeper tooling):
previewHarvestFeesToRewards() -> (feeWeth, feeClaim, expectedClaimOut, bountyWethEstimate)- does not call
claimFees()(reports current balances) feeClaimis the current unaccounted CLAIM balance after excluding the already-accounted rewards reserve; it is not the vault’s full CLAIM balanceexpectedClaimOutis a best-effort spot quote (can be 0)
- does not call
Bounty constants (v1.0.0):
STALE_BOUNTY_AFTER = 60 minutesBOUNTY_BPS = 100(1%)MAX_BOUNTY_WETH = 0.01 ETH(in WETH)
Bounty rules:
- if last harvest was within
STALE_BOUNTY_AFTER: bounty = 0 - if harvest is stale: bounty =
min(MAX_BOUNTY_WETH, feeWeth * 1%)
minClaimOut handling:
- if
wethToSwap > 0,minClaimOutMUST be non-zero - if
wethToSwap == 0, runtime clamps the effective floor to0and ignores the caller’sminClaimOut - the vault enforces an additional onchain sanity floor derived from an Aerodrome spot quote:
effectiveMinClaimOut = max(minClaimOut, quoteFloor)when a swap occursquoteFloor = quotedOut * (1 - HARVEST_MAX_SLIPPAGE_BPS)- v1.0.0:
HARVEST_MAX_SLIPPAGE_BPS = 100(1%)
- this does not prevent sandwich attacks (quote and swap read the same manipulable state)
- keepers MUST compute
minClaimOutfrom a TWAP/oracle price - keepers SHOULD submit via a private relay for MEV protection
- keepers MUST compute
Auto-compound (vault rewards -> veCLAIM)
Users can opt in to auto-compound their vault CLAIM rewards into the Furnace.
Key rules (v1.0.0):
- disabled by default (explicit opt-in)
- does not create new locks in v1.0.0
- a destination
tokenIdis required
- a destination
- execution is owner-or-keeper-allowlisted
owneror an allowlistedisHarvestKeeper
Config per user (onchain):
enabled,pausedtokenIddestination- must be owned by the user
- must not be listed
- must not be expired
durationSeconds(stored target remaining duration)- validated when saving config (MIN..MAX; AutoMax requires MAX)
- execution uses
max(cfg.durationSeconds, lockEnd - now)for normal locks, orMAX_LOCK_DURATIONfor AutoMax - adding capital does not change the lock’s duration
maxSlippageBps0= useDEFAULT_LP_AUTOCOMPOUND_MAX_SLIPPAGE_BPS(v1.0.0: 300 = 3%)- clamped to
<= 1_000(10%); revertsSlippageTooHighabove that
Config methods:
setAutoCompoundConfig(enabled, tokenId, durationSeconds, maxSlippageBps, minRewardToCompound)getAutoCompoundConfig(user) -> (enabled, paused, tokenId, durationSeconds, maxSlippageBps, minRewardToCompound)- Delegation-gated setter:
setAutoCompoundConfigForUser(user, ...)(requiresP_SET_LP_AUTOCOMPOUND_CONFIG_FOR)- auth fails closed on bundle drift (see Wiring safety model)
Execution (vault owner or allowlisted harvest keeper only):
compoundFor(user)compoundForMany(users[], maxUsers)- processes up to
min(users.length, maxUsers, MAX_LP_COMPOUND_USERS_PER_CALL) - v1.0.0:
MAX_LP_COMPOUND_USERS_PER_CALL = 50
- processes up to
Slippage model (important):
- keepers do not pass
minVeOut - the vault computes
minVeOutfrom a Furnace spot quote and the user’smaxSlippageBps - if the quote fails, the user is skipped (no unsafe execution)
Eligibility + pause behavior:
- best-effort per user (batch does not revert for a single failure)
- skips when
earned(user) < minCompoundReward(owner-settable global floor; default 1 CLAIM) MIN_COMPOUND_INTERVAL(1 day) enforced between auto-compound calls per user (vialastCompoundTs); user-initiated Harvest & Lock (claimRewardsAndLock) has an independent cooldown (vialastUserLockTs)- pauses config if the destination lock becomes ineligible at execution time
- not owner / listed / expired / invalid tokenId
- user must save a new config to resume
- auto-compound pauses automatically when the destination non-AutoMax lock’s remaining time drops below
MIN_LOCK_DURATION(7 days), using theEXPIREDpause reason (code 3) - pause reason codes reuse
Constants.SHAREHOLDER_AUTOCOMPOUND_PAUSE_REASON_*(1..5; code 5 =FURNACE_REVERTon failed Furnace entry)
UI/UX integration (recommended):
- Rewards actions should mirror Royalties:
- Primary (default): Harvest CLAIM
- Secondary: Harvest & Lock
- Optional: “Remember my choice” toggle (local preference)
- After a successful Harvest & Lock, prompt: “Enable Auto-compound?” (routes to config modal, prefilled)
notifyRewards allowlist
To prevent arbitrary reward injection, notifyRewards is allowlisted.
Authorized sources (v1.0.0):
- Furnace (LP reward split)
- this vault itself (
address(this)remains allowlisted for vault-side notifier flows; currentharvestFeesToRewardsuses the same balance-delta accounting path without an external self-call)
Operational note:
- Upstream funders may treat
notifyRewardsas best-effort (swallow reverts) to avoid DoS on unrelated flows. - This vault uses balance-delta accounting (
amountClaimis ignored), so any CLAIM transferred without a successful notify can still be accounted on a later successful notify (includingnotifyRewards(0)).
Integrator notes
- If you display APR, use the canonical APR spec in
docs/spec/apr-calculation-spec-v1.0.0.md. - Do not label CLAIM rewards as “claim” in UI. Use Harvest.
See also
- Furnace — LP overflow drip and lock bonus engine
- Maintenance and Bots — harvest and compound keeper loops
- Core Mechanics — emission schedule and LP stream
- Constants Reference — reward durations and slippage bounds
- User manual: Liquidity & LP Vault