Skip to Content
LP Staking Vault

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 stakedBalance immediately
    • creates an onchain unbond entry (id, amount, unlockTime)
  • Constants (v1.0.0):
    • UNBONDING_PERIOD = 7 days
    • MAX_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.tokenId is the actual destination lock id returned by Furnace; if a new lock is created, index the minted token id rather than assuming 0

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’s feeClaim / claimToRewards reflect 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

Preview helper (UI / keeper tooling):

  • previewHarvestFeesToRewards() -> (feeWeth, feeClaim, expectedClaimOut, bountyWethEstimate)
    • does not call claimFees() (reports current balances)
    • feeClaim is the current unaccounted CLAIM balance after excluding the already-accounted rewards reserve; it is not the vault’s full CLAIM balance
    • expectedClaimOut is a best-effort spot quote (can be 0)

Bounty constants (v1.0.0):

  • STALE_BOUNTY_AFTER = 60 minutes
  • BOUNTY_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, minClaimOut MUST be non-zero
  • if wethToSwap == 0, runtime clamps the effective floor to 0 and ignores the caller’s minClaimOut
  • the vault enforces an additional onchain sanity floor derived from an Aerodrome spot quote:
    • effectiveMinClaimOut = max(minClaimOut, quoteFloor) when a swap occurs
    • quoteFloor = 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 minClaimOut from a TWAP/oracle price
    • keepers SHOULD submit via a private relay for MEV protection

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 tokenId is required
  • execution is owner-or-keeper-allowlisted
    • owner or an allowlisted isHarvestKeeper

Config per user (onchain):

  • enabled, paused
  • tokenId destination
    • 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, or MAX_LOCK_DURATION for AutoMax
    • adding capital does not change the lock’s duration
  • maxSlippageBps
    • 0 = use DEFAULT_LP_AUTOCOMPOUND_MAX_SLIPPAGE_BPS (v1.0.0: 300 = 3%)
    • clamped to <= 1_000 (10%); reverts SlippageTooHigh above that

Config methods:

  • setAutoCompoundConfig(enabled, tokenId, durationSeconds, maxSlippageBps, minRewardToCompound)
  • getAutoCompoundConfig(user) -> (enabled, paused, tokenId, durationSeconds, maxSlippageBps, minRewardToCompound)
  • Delegation-gated setter:
    • setAutoCompoundConfigForUser(user, ...) (requires P_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

Slippage model (important):

  • keepers do not pass minVeOut
  • the vault computes minVeOut from a Furnace spot quote and the user’s maxSlippageBps
  • 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 (via lastCompoundTs); user-initiated Harvest & Lock (claimRewardsAndLock) has an independent cooldown (via lastUserLockTs)
  • 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 the EXPIRED pause reason (code 3)
  • pause reason codes reuse Constants.SHAREHOLDER_AUTOCOMPOUND_PAUSE_REASON_* (1..5; code 5 = FURNACE_REVERT on 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; current harvestFeesToRewards uses the same balance-delta accounting path without an external self-call)

Operational note:

  • Upstream funders may treat notifyRewards as best-effort (swallow reverts) to avoid DoS on unrelated flows.
  • This vault uses balance-delta accounting (amountClaim is ignored), so any CLAIM transferred without a successful notify can still be accounted on a later successful notify (including notifyRewards(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