Docs · reference

How the vault works, line by line.

What StashVault counts, how it prices a harvest, how a gain reaches the share rate and what each key can and cannot do. Every rule comes with the constant, the selector and the source line behind it, so you can check it instead of taking it on trust.

Source
src/StashVault.sol src/StashFactory.sol src/lib/TwapOracle.sol
Tests · 23 Sep 2026
318 offline in 16 suites · 4 against live chain state · fuzz 256 runs · all green
Status
Reading config/addresses.json…
On this pageOverview

01 / 15

01 · Overview

One vault per coin, and four things that happen to it.

A STASH vault holds one coin, the asset. Deposit it and you receive shares, named s plus the coin's ticker (sUSDG for USDG). The coin pays its holders something else, the reward: trading fees, ETH, a stablecoin, a stock token. The keeper sells that reward for more of the asset, and the gain is released into the share rate over the vesting window. Redeem and you get the asset back, in kind, at the current rate.

The factory creates one vault per coin and then steps away: it holds no funds and has no power over a vault once it is deployed. The vault has no proxy, no mint entrypoint, no blacklist, no transfer hook and no tax setter, and its asset, reward, router and pool fee are fixed at creation.

1
vault per coin · VaultExists
4
keys at creation: admin, guardian, treasury, keeper
0
proxies and mint entrypoints

What each event does to the four numbers

rawAssets · totalAssets · totalSupply · rate
Effect of each event on the vault's balance, its counted assets, its share supply and the share rate
EventrawAssets()coin heldtotalAssets()coin countedtotalSupply()sharesShare ratecoin per share
Deposit↑↑↑unchanged
A harvest lands↑==unchanged at that instant
The lock releases=↑=rises in a straight line
Redeem↓↓↓unchanged
Coin sent straight in↑↑=rises at once
The coin's price falls===unchanged: it counts coin
Rounding in deposit and redeem always goes the vault's way, so neither can lower the rate. No vault function lowers it: the invariant suite checks that after every operation. It rises when a harvest vests, and coin sent straight to the vault raises it too.

The files

  • src/StashVault.solthe vault: shares, the rate, deposit, harvest, redeem, the roles and their limits
  • src/StashFactory.solcreates one vault per coin, refuses duplicate keys and missing pools, can retire a vault
  • src/lib/TwapOracle.solthe pool's 30-minute average price, vendored from Uniswap v3-periphery
  • src/lib/FullMath.sol512-bit mulDiv for every conversion, so large balances cannot overflow

02 · Accounting

What the vault counts, and what it leaves out.

totalAssets()
balance minus what is still vesting
+3
share decimals over the coin
1,000
virtual shares, 1 virtual coin unit

Three views describe the coin inside the vault. rawAssets() is the asset's balance, vested or not. lockedProfitRemaining() is the part of the last harvest that has not been released yet. totalAssets() is the first minus the second: the principal the shares have a claim on, and the only number the rate is made of.

Reward tokens waiting in the vault are inventory, not principal. They count for nothing until a harvest turns them into the asset. That is also why a vault whose asset is its own reward is refused at construction (AssetIsReward): its inventory would count as principal and anyone could move the rate by sending it in.

The vault's balance, split the way the contract splits it

Replay dry run · local fork

rawAssets() coin held

12,474.511185 USDG

totalAssets() counted 11,237.255592

lockedProfitRemaining() still vesting 1,237.255593

reward inventory, not counted 0 WETH

totalSupply() shares

10,000.000000000 sUSDG

share rate 1.123725559 the lock is half released; the rate is half way up

Dry run at 3 hours after the harvest: 12,474.511185 USDG held, 11,237.255592 counted, 1,237.255593 still vesting, share rate 1.123725559.

Replay of a test run on a local fork of Robinhood Chain, 22 Sep 2026, nothing broadcast: 10,000 USDG in, 1 WETH harvested. The 3 h values are the contract's own linear release of that harvest. Test sizes, not a yield forecast.

Virtual shares

Every conversion adds 1,000 virtual shares against one virtual unit of the asset: shares = assets × (totalSupply + 1,000) ÷ (totalAssets + 1) and back again. This is the first-depositor and donation-inflation guard: someone who deposits one unit and then donates to the empty vault cannot capture more than they give, and loses about half of it. It is also why an empty vault opens at a rate of exactly 1e27.

All four conversions (convertToShares, convertToAssets, deposit, redeem) and the rate use a 512-bit mulDiv. In 256 bits the products overflowed on large balances of big-supply coins; they cannot now.

Share decimals: the coin's plus three

USDG · 6 decimals1.000000
sUSDG · 9 decimals1.000000000

decimals() on the share returns the asset's decimals plus DECIMALS_OFFSET = 3, so one share still reads as about one coin in a wallet while the three extra digits absorb rounding. A coin with fewer than four decimals is refused (UnsupportedDecimals): there, the single virtual unit is a large piece of a whole coin, and a one-unit deposit stranded half of every harvest in a share nobody could redeem. More than 18 is refused too.

03 · The share rate

One formula: exchangeRateRay()

The share rate is how much coin one whole share is worth, scaled by 1027 (a ray) so it keeps its precision in integer maths. The site prints it divided by 1e27, to nine decimals.

exchangeRateRay() · selector 0x54b3b33c

src/StashVault.sol:331
1,0001×1e272×(totalAssets()3+14)
totalSupply()5+1,0006
  1. VIRTUAL_SHARES = 103. Share decimals are coin decimals + 3, so this factor turns "coin per share-unit" into "coin per whole share" with no exponent.
  2. RAY = 1027: the rate's own precision.
  3. totalAssets(): the coin the vault counts, its balance minus the part still vesting.
  4. One virtual unit of coin, so an empty vault is never a division by zero.
  5. totalSupply(): shares outstanding.
  6. The 1,000 virtual shares that sit against that one virtual unit.

An empty vault1,000 × 1e27 × (0 + 1) ÷ (0 + 1,000) = 1e271.000000000

Dry run, fully vested1,000 × 1e27 × (12,474,511,185 + 1) ÷ (10,000,000,000,000 + 1,000)1.247451118

Base units: USDG has 6 decimals and sUSDG 9, so 12,474,511,185 is 12,474.511185 USDG and 10,000,000,000,000 is 10,000 sUSDG. Dry run on a local fork, 22 Sep 2026, nothing broadcast.

Why it never falls

  • Deposits and redemptions change the coin and the shares in proportion, and both round down in the vault's favour, so the ratio can only stay or edge up by a rounding unit.
  • At the instant of a harvest the balance and the lock rise by the same amount, so totalAssets() does not move. The rate rises only as the lock releases, in a straight line.
  • Coin sent straight to the vault raises it at once. Nobody loses by that: a depositor still gets back what their shares are worth. It is one reason every vault is seeded before it opens.
  • No function lowers it. The invariant suite runs fuzzed sequences of deposits, harvests, redemptions, donations and admin calls and checks after every step that the rate is at least 1e27 and at least what it was.

The Harvest event records rateRayAfter, the rate at the instant of that harvest, which is before its gain has vested. For the current value, call exchangeRateRay().

04 · Deposit

Deposit: shares for what actually arrived.

deposit(uint256 assets, address receiver, uint256 minSharesOut)selector 0xbc157ac1 · anyone · src/StashVault.sol:366 · the two-argument ERC-4626 form 0x6e553f65 calls it with minSharesOut = 0

Approve the vault for the coin first (the coin's own approve), then deposit. The call runs these checks in this order; any one of them reverts the whole transaction, and the error names which.

  1. Deposits paused? The guardian or the admin can pause them.

    :371

    DepositsArePaused()0x5a65d188

  2. Receiver is the zero address?

    :372

    ZeroAddress()0xd92e233d

  3. Zero assets?

    :373

    ZeroAmount()0x1f2a2005

  4. Pull the coin. totalAssets() is read before and after transferFrom.

    :375-378

    transferFrom

  5. Measure what arrived. A coin with a transfer tax lands short; shares are minted on what landed, not on what was asked.

    :382-383

    ZeroAmount()0x1f2a2005

  6. Price the shares. received × (supply + 1,000) ÷ (assetsBefore + 1), 512-bit, rounded down.

    :387-388

    ZeroShares()0x9811e0c7

  7. Fewer shares than you asked for? Your floor, minSharesOut.

    :389

    SlippageShares(got, wanted)0x3e0dcb4a

  8. Mint the shares to the receiver and emit Deposit.

    :391-392

    mint · Deposit

previewDeposit is an upper bound: with a taxed coin the real count can be lower. Set minSharesOut from the preview minus the slippage you accept; the app does exactly that, in integer maths, for every deposit.

05 · Harvest

Harvest: the payout becomes more of the coin.

harvest(uint256 minAssetsOut)selector 0xddc63262 · keeper only · src/StashVault.sol:441 · mints no shares

The keeper is the only address that can call it. It sells the vault's whole reward balance for the asset on the pool fixed at creation, through Uniswap v3's SwapRouter02, and books the gain into the vesting lock. No share is minted: supply stays where it is, and the coin per share goes up as the lock releases.

  1. Caller is not the keeper?

    :442

    NotKeeper()0xf512b278

  2. Harvests paused? Guardian or admin.

    :443

    HarvestIsPaused()0xe9dd7496

  3. Too soon? The first harvest is free; after that, one per harvestInterval.

    :445-446

    HarvestTooSoon()0xcb562bd0

  4. No reward to sell?

    :448-449

    NothingToHarvest()0x3f29331a

  5. Take the fee in the reward token: harvested × feeBps ÷ 10,000 to the treasury. Never from principal.

    :451-452

    fee → treasury

  6. Measure what is left to sell (the reward may be taxed too).

    :455-456

    NothingToHarvest()0x3f29331a

  7. Price the floor from the pool before the swap: the higher of harvestFloor(swapIn) and your minAssetsOut. Too small to be worth one unit of the asset, or no price history, and there is no harvest.

    :460-463

    UnpricedHarvest()0x557bb5af

  8. Swap: approve the router for exactly the amount, exactInputSingle reward → asset with amountOutMinimum 0, then set the allowance back to 0.

    :467-482

    swap · allowance 0

  9. Gained less than the floor? Checked on the vault's own measured gain, not on the router's figure.

    :485-486

    SlippageAssets(got, wanted)0x806e3e09

  10. Lock the gain, record lastHarvest and emit Harvest. Set last, so a reverted harvest does not use up the interval.

    :490-505

    lock · Harvest

The throttle

After the first harvest, the next one has to wait harvestInterval: 120 seconds by default. The admin can raise it to 7 days or lower it to 60 seconds, not below: a keeper that could harvest every block could repeat a bad-price harvest every block. It can never be longer than the vesting window, or a week of yield could pile into one release.

Time between harvests: what the admin may set

log scale · 1 min to 7 days
Solid: always allowed. Dashed: allowed only after the vesting window has been made at least that long. IntervalTooShort, IntervalTooLong and IntervalAboveVesting refuse the rest.

What the keeper decides, and what it does not

The keeper decides when. It can pass a minAssetsOut above the pool's floor and that stricter number binds; a looser one is ignored. It cannot pick the pool, the router or the fee tier, cannot harvest more often than the throttle, cannot skip the fee split, and never touches principal. If it disappears, nothing is harvested and payouts wait in the vault; redeem does not depend on it, and the admin can appoint another.

06 · Price floor maths

The price floor comes from the pool, not from the keeper.

Before it swaps, the vault asks the pool the swap goes through what it has been paying on average. harvestFloor(swapIn) takes the pool's mean tick over twapWindow (30 minutes by default), converts swapIn at that tick, and takes off the pool's own fee and the tolerance, in one 512-bit step:

floor = quoteAtTick(meanTick(pool, twapWindow), swapIn) × (1,000,000 − poolFee) ÷ 1,000,000 × (10,000 − maxSlippageBps) ÷ 10,000 src/StashVault.sol:349 · selector 0xf96054b8 · the binding floor is the higher of this and the keeper's minAssetsOut

A 1,500 WETH dump in the same block, against the floor

Below the floor. The harvest reverts with SlippageAssets; the reward stays in the vault.

Quote for 0.9 WETH before the dump: 2,477.78 USDG, above the floor of 2,450.62. After a 1,500 WETH dump in the same block: 2,369.67 USDG, below the floor, which did not move. The harvest reverted.

Hatched: the band the keeper may sell into, the pool fee plus the 1% tolerance below the 30-minute average. fork test against live state, 22 Sep 2026 · 0.01% WETH/USDG pool

How far below the average, per fee tier

With the default 1% tolerance, the floor is this fraction of the 30-minute quote, for each tier of the WETH/USDG pools on Robinhood Chain:

Pool feepoolFeeFloor ÷ 30-min quoteBand under the average
0.01%1000.9899011.0099%
0.05%5000.9895051.0495%
0.3%30000.9870301.2970%
1%100000.9801001.9900%

The rules around it

  • One venue. The pool is looked up at construction from the router's own factory, never passed in, so the floor and the swap read the same pool.
  • No history, no harvest. If the pool cannot answer for the whole window (too young, or too little observation history), observe() fails and the harvest reverts with TwapUnavailable(pool, window). The keeper's number never stands in for a missing price.
  • Can be loosened, never switched off. The admin can set the window from 10 minutes to 2 hours and the tolerance up to 10%, not beyond.
  • Checked on the vault's own gain. The router is given a minimum of zero; the vault compares what actually arrived with the floor. A coin with a transfer tax loses the tax on every fill, so its vault needs a tolerance that covers the tax, or every harvest reverts, loudly.
  • Upstream maths. meanTick rounds towards negative infinity as upstream does, and getQuoteAtTick is the upstream body unchanged, vendored from Uniswap v3-periphery's OracleLibrary so the contracts have no external dependency.

07 · Vesting maths

Vesting: a gain reaches the rate in a straight line.

A harvest does not hand its gain to the shares at once. It books the gain into a lock, and the lock releases linearly until lockedProfitUntil:

lockedProfitRemaining() = lockedProfit − lockedProfit × elapsed ÷ span span = lockedProfitUntil − lockedProfitAt · 0 once the end is reached · src/StashVault.sol:264 · selector 0x0dc3f9cd

The division floors the released part, not the remaining one, so the lock rounds in the vault's favour: at most one unit is released later than a perfectly straight line, never one earlier. The remaining lock is also capped at the balance, defensively.

Six hours of one harvest, replayed through the contract's formulas

Replay dry run · local fork
time after the harvest6 h 00 min
still locked0.000000
counted, totalAssets()12,474.511185
share rate1.247451118
12,474.511185 held 10,000.000000 deposited harvest · rate unchanged fully vested
rawAssets() heldtotalAssets() countedlocked, still vestingreleased, now in the rate
Replay of a test run on a local fork of Robinhood Chain, 22 Sep 2026, nothing broadcast: 10,000 USDG in, 1 WETH harvested for 2,474.511185 USDG. Every value is computed in your browser with the contract's integer maths. Test sizes, not a yield forecast.
The same six hours, hour by hour
After the harvestStill lockedCountedShare rate
0 h2,474.51118510,000.0000001.000000000
1 h2,062.09265510,412.4185301.041241852
2 h1,649.67412410,824.8370611.082483706
3 h1,237.25559311,237.2555921.123725559
4 h824.83706211,649.6741231.164967412
5 h412.41853112,062.0926541.206209265
6 h0.00000012,474.5111851.247451118

A running lock keeps its own dates

The lock carries its own start (lockedProfitAt) and end (lockedProfitUntil). setVestingPeriod changes only the window the next harvest will use: lengthening it cannot push principal down, and shortening it cannot pull an outstanding gain forward. When a new harvest lands while a lock is still running, the unreleased remainder is carried into the new lock with the new gain, the clock restarts at that moment, and the end becomes the later of now + vestingPeriod and the old end. It is never pulled earlier.

A second harvest while the first is still releasing

Illustration not to scale
1 2 3 4 5
  1. 1Harvest 1 locks its gain; the lock falls in a straight line towards its end.
  2. 2Harvest 2 lands early: what is still locked is carried over, not released.
  3. 3The new gain is added on top, and the combined lock starts falling from now.
  4. 4Where the first lock would have ended (dashed: the path it no longer takes).
  5. 5The new end: the later of now + the vesting window and the old end. Never earlier.

08 · Redeem

Redeem: the same coin back, at the current rate.

redeem(uint256 shares, address receiver, address owner, uint256 minAssetsOut)selector 0x9f40a7b3 · anyone (the owner, or someone the owner approved) · src/StashVault.sol:402 · the three-argument form 0xba087652 calls it with minAssetsOut = 0

In kind: you get the vault's asset, never the reward and never anything else. No exit fee and no queue. The one exception is an admin redemption pause, which lasts 72 hours at most (see Pauses).

  1. Redemption paused? Only the admin can pause it, for 72 hours at most.

    :407

    RedeemIsPaused()0xd67e879f

  2. Receiver is the zero address?

    :408

    ZeroAddress()0xd92e233d

  3. Zero shares?

    :409

    ZeroAmount()0x1f2a2005

  4. Redeeming someone else's shares? Their allowance is spent, unless it is unlimited.

    :411-417

    ERC20InsufficientAllowance()0x2fc50d60

  5. Price the coin. shares × (totalAssets + 1) ÷ (totalSupply + 1,000), 512-bit, rounded down. The part still vesting is not counted, so it keeps releasing to the shares that stay.

    :419-420

    convert

  6. Clamp to what is counted. Defensive: a 256-run fuzz shows it never engages.

    :423

    clamp

  7. Nothing to pay, or less than your floor?

    :424-425

    SlippageAssets(got, wanted)0x806e3e09

  8. Burn the shares, send the coin and emit Withdraw.

    :427-430

    burn · transfer · Withdraw

In the dry run, redeeming every share six hours after the harvest paid out 12,474.511184 USDG on a 10,000 deposit, and one unit, 0.000001 USDG, stayed in the vault: rounding in its favour. dry run on a local fork, nothing broadcast

09 · Roles and powers

Four keys, and exactly how far each one reaches.

The factory refuses to create a vault unless admin, guardian, treasury and keeper are four different non-zero addresses (DuplicateRole, 0x86afdf28). After that, the admin can reassign the guardian, the treasury and the keeper; those setters only check for a non-zero address. The admin role itself moves in two steps: transferAdmin names a successor and acceptAdmin, called by that successor, completes it.

A

Admin

one per vault

Can

  • set the fee, up to 30% of the payout
  • set the harvest interval, vesting window, price window and tolerance, within the walls below
  • reassign guardian, treasury and keeper
  • pause and unpause deposits and harvests
  • pause redemption for up to 72 h
  • rescue a token sent by mistake
  • hand the role over, in two steps

Cannot

  • mint shares or move the asset or the reward
  • change the asset, reward, router or fee tier
  • lower the rate, or pull a running lock forward
  • pause redemption again during the 72 h lockout
  • switch off the price floor or the throttle
G

Guardian

brakes only

Can

  • pause and unpause deposits
  • pause and unpause harvests

Cannot

  • pause redemption, ever
  • change any setting or move any token
K

Keeper

the only harvester

Can

  • call harvest, once per interval
  • make the floor stricter with minAssetsOut
  • choose the moment, within the band

Cannot

  • loosen the floor or pick another pool
  • skip the fee or touch principal
  • take a harvest in one instant: it vests
T

Treasury

receives the fee

Gets

  • the fee, in the reward token, at each harvest

Cannot

  • call anything the public cannot; it is an address, not a power
F

Factory admin

creates, then steps away

Can

  • create one vault per coin, with four distinct keys and an existing pool
  • retire a vault, so a replacement can be created; its holders keep every right, redeem included

Cannot

  • touch a vault's funds or settings once it exists
  • reuse a retired vault's address: the index is in the CREATE2 salt

The admin's settings, and the walls it cannot pass

Settings: the range allowed, the default, the hard wall

src/StashVault.sol:46-75

Percentages · linear

Fee of the payout

10% default · wall 30%

Floor tolerance under the average

1% default · wall 10%

Time windows · log scale

Vesting window next harvest only

6 h default · 10 min – 7 days

Price window the TWAP

30 min default · 10 min – 2 h

Harvest interval ≤ the vesting window

120 s default · 60 s – 7 days

A setter outside the walls reverts: FeeTooHigh, SlippageTooHigh, VestingTooShort / VestingTooLong, TwapWindowOutOfRange, IntervalTooShort / IntervalTooLong / IntervalAboveVesting. Every change emits an event with the old and the new value.

Rescue

rescue(token, to, amount) sweeps a token sent to the vault by mistake. It refuses the asset, the reward and the share token by address, and then, because a token can have two contract addresses over one ledger, it snapshots the asset and reward balances and reverts with CannotRescueCore if either one moved. It is guarded on the effect, not on the name.

10 · Pauses

Three switches. None of them touches the rate.

SwitchWhoLimitWhile it is on
Depositsguardian or adminnone: on until switched offmaxDeposit returns 0; deposit reverts DepositsArePaused
Harvestsguardian or adminnone: on until switched offpayouts wait in the vault; the rate stays where the lock leaves it
Redemptionadmin only72 h at most, then a 72 h lockoutmaxRedeem returns 0; redeemPaused() is true; redeem reverts RedeemIsPaused

The longest a redemption pause can last, and what follows it

MAX_REDEEM_PAUSE · REDEEM_PAUSE_COOLDOWN

A full pause

Lifted early

A redemption pause lasts at most 72 hours. The admin cannot start another pause until 72 hours after the pause's scheduled end. Lifting a pause early reopens redemption at once but does not shorten that lockout.

unpauseRedeem() reopens redemption at once, but the lockout still runs to 72 hours after the pause's scheduled end. The guardian has no part in any of this.

11 · When a vault opens

A vault opens only when its yield is real.

The contract cannot tell a real payout from a buyback, so these are the project's rules, applied before a vault takes a deposit. The evidence for each goes on Proof as transaction hashes.

Source A · the creator signs

The coin's creator points its fee recipient at the vault and signs that change themselves.

Source B · twenty real payouts

A vault-shaped test account receives 20 payouts from the coin first, each with its own hash.

  • Buybacks don't count. A buyback pays no holder, so there is nothing for a vault to receive.
  • No dead coins. The factory refuses a pair with no pool (PoolDoesNotExist(asset, reward, fee), 0x137b894c), and the create script refuses an empty one: the chain cannot tell a thin pool from a healthy one.
  • One vault per coin. A second is refused (VaultExists, 0x70743b38). A vault created on the wrong fee tier, or griefed before its seed deposit, can be retired with retireVault so a replacement can open; its holders keep every right, redeem included.
  • Seeded first. Every vault gets a first deposit from the project before it opens, so an empty vault is never the target of a donation.

12 · ERC-4626 deviations

Where it leaves ERC-4626, on purpose.

The share accounting is ERC-4626 in shape: asset(), totalAssets(), the converters, the previews and the standard Deposit and Withdraw events. Two of the standard four entrypoints promise an exact amount out. With a coin that charges a transfer tax, what arrives is only known after the pull, so those two are left out instead of being wrong.

ERC-4626HereWhy
mint(shares)absentan exact share count cannot be promised when the coin arrives short
withdraw(assets)absentsame reason, the other way round
deposit(assets, receiver)keptcalls the three-argument form with minSharesOut = 0
redeem(shares, receiver, owner)keptcalls the four-argument form with minAssetsOut = 0
floors on bothaddedminSharesOut and minAssetsOut: revert instead of paying less
previewDepositupper bounda taxed coin mints fewer shares than previewed
maxDeposit(address)ignores the argument0 while deposits are paused, otherwise unlimited
maxRedeem(owner)pause-aware0 during a redemption pause, otherwise the owner's balance
totalAssets()net of the lockexcludes the part still vesting and all reward inventory
decimals()asset + 3one share reads as about one coin; the extra digits absorb rounding
previewMint, previewWithdraw, maxMint, maxWithdrawabsentthey belong to the two entrypoints that are not there

13 · Contract reference

Every function, event and error, with its selector.

Selectors are the first four bytes of keccak256 of the signature; an event's topic0 is the full hash. They were computed with cast sig and cast keccak (Foundry 1.8.1) from the source, and every value printed here was recomputed with cast on 24 Sep 2026 and matched.

StashVault · functions 86

signatureselectorwho / typesource
RAY()0x552033c4constantStashVault.sol:31
BPS()0x249d39e9constantStashVault.sol:32
DECIMALS_OFFSET()0xaea70accconstantStashVault.sol:36
MIN_ASSET_DECIMALS()0xdd7e15a7constantStashVault.sol:39
VIRTUAL_SHARES()0x88c47f68constantStashVault.sol:44
MAX_FEE_BPS()0xd55be8c6constantStashVault.sol:46
DEFAULT_FEE_BPS()0x33d8262cconstantStashVault.sol:47
DEFAULT_HARVEST_INTERVAL()0xd57d4ad5constantStashVault.sol:48
MAX_HARVEST_INTERVAL()0x0d15e044constantStashVault.sol:49
MIN_HARVEST_INTERVAL()0xe8343a66constantStashVault.sol:52
DEFAULT_VESTING_PERIOD()0x098480ecconstantStashVault.sol:58
MIN_VESTING_PERIOD()0xc85fcbe8constantStashVault.sol:59
MAX_VESTING_PERIOD()0xe5a5e674constantStashVault.sol:60
MAX_REDEEM_PAUSE()0xe34f7145constantStashVault.sol:61
REDEEM_PAUSE_COOLDOWN()0xa0c62586constantStashVault.sol:62
DEFAULT_TWAP_WINDOW()0x5fcb2120constantStashVault.sol:68
MIN_TWAP_WINDOW()0x1a65893bconstantStashVault.sol:69
MAX_TWAP_WINDOW()0xb88a92b4constantStashVault.sol:70
DEFAULT_MAX_SLIPPAGE_BPS()0xcdb994e9constantStashVault.sol:74
SLIPPAGE_CAP_BPS()0xf39fdeb1constantStashVault.sol:75
asset()0x38d52e0fimmutable · address (IERC20)StashVault.sol:82
reward()0x228cb733immutable · address (IERC20)StashVault.sol:84
router()0xf887ea40immutable · address (ISwapRouter02)StashVault.sol:86
poolFee()0x089fe6aaimmutable · uint24StashVault.sol:88
pool()0x16f0115bimmutable · addressStashVault.sol:91
assetDecimals()0xc2d41601immutable · uint8StashVault.sol:92
admin()0xf851a440storage · addressStashVault.sol:96
pendingAdmin()0x26782247storage · addressStashVault.sol:97
guardian()0x452a9320storage · addressStashVault.sol:98
treasury()0x61d027b3storage · addressStashVault.sol:99
keeper()0xaced1661storage · addressStashVault.sol:100
feeBps()0x24a9d853storage · uint256StashVault.sol:102
harvestInterval()0x14dc0ce0storage · uint64StashVault.sol:103
lastHarvest()0xf1a392dastorage · uint64StashVault.sol:104
vestingPeriod()0x7313ee5astorage · uint64StashVault.sol:108
lockedProfit()0x44b81396storage · uint256StashVault.sol:110
lockedProfitAt()0xbc028917storage · uint64StashVault.sol:111
lockedProfitUntil()0x049a4041storage · uint64StashVault.sol:114
depositsPaused()0x60da3e83storage · boolStashVault.sol:116
harvestPaused()0x8a3cf676storage · boolStashVault.sol:117
redeemPausedUntil()0xa8378459storage · uint64StashVault.sol:118
redeemPauseCooldownUntil()0xfd61a56fstorage · uint64StashVault.sol:119
twapWindow()0x8107e133storage · uint32StashVault.sol:121
maxSlippageBps()0xc4aa7395storage · uint256StashVault.sol:122
rawAssets()0x4115e031view · public viewStashVault.sol:258
lockedProfitRemaining()0x0dc3f9cdview · public viewStashVault.sol:264
totalAssets()0x01e1d114view · public viewStashVault.sol:291
convertToShares(uint256)0xc6e6f592view · public viewStashVault.sol:299
convertToAssets(uint256)0x07a2d13aview · public viewStashVault.sol:303
previewDeposit(uint256)0xef8b30f7view · external viewStashVault.sol:309
previewRedeem(uint256)0x4cdad506view · external viewStashVault.sol:313
maxDeposit(address)0x402d267dview · external viewStashVault.sol:317
maxRedeem(address)0xd905777eview · external viewStashVault.sol:321
exchangeRateRay()0x54b3b33cview · public viewStashVault.sol:331
redeemPaused()0xb235d468view · external viewStashVault.sol:335
harvestReady()0x779693faview · external viewStashVault.sol:339
harvestFloor(uint256)0xf96054b8view · public viewStashVault.sol:349
deposit(uint256, address)0x6e553f65deposit · redeem · harvest · anyoneStashVault.sol:359
deposit(uint256, address, uint256)0xbc157ac1deposit · redeem · harvest · anyoneStashVault.sol:366
redeem(uint256, address, address)0xba087652deposit · redeem · harvest · anyone (owner or approved)StashVault.sol:397
redeem(uint256, address, address, uint256)0x9f40a7b3deposit · redeem · harvest · anyone (owner or approved; spends allowance if msg.sender != owner)StashVault.sol:402
harvest(uint256)0xddc63262deposit · redeem · harvest · **keeper only** — if (msg.sender != keeper) revert NotKeeper();StashVault.sol:441-442
transferAdmin(address)0x75829defadmin · onlyAdminStashVault.sol:510
acceptAdmin()0x0e18b681admin · **pendingAdmin only** — if (msg.sender != pendingAdmin) revert NotPendingAdmin();StashVault.sol:516-517
setGuardian(address)0x8a0dac4aadmin · onlyAdminStashVault.sol:524
setTreasury(address)0xf0f44260admin · onlyAdminStashVault.sol:530
setKeeper(address)0x748747e6admin · onlyAdminStashVault.sol:536
setFeeBps(uint256)0x72c27b62admin · onlyAdmin (reverts FeeTooHigh above MAX_FEE_BPS)StashVault.sol:542
setHarvestInterval(uint64)0x40baf51dadmin · onlyAdminStashVault.sol:548
setVestingPeriod(uint64)0xf4768256admin · onlyAdminStashVault.sol:562
setTwapWindow(uint32)0x5f553545admin · onlyAdminStashVault.sol:571
setMaxSlippageBps(uint256)0x25fc1b3dadmin · onlyAdminStashVault.sol:579
setDepositsPaused(bool)0x1c481e2dadmin · onlyGuardianOrAdminStashVault.sol:587
setHarvestPaused(bool)0x40a02887admin · onlyGuardianOrAdminStashVault.sol:592
pauseRedeem(uint64)0x4efc7530admin · onlyAdmin — **guardian has no say** (line 597-598 comment)StashVault.sol:599
unpauseRedeem()0xaf3345d1admin · onlyAdminStashVault.sol:612
rescue(address, address, uint256)0x20ff430badmin · onlyAdmin; refuses asset/reward/address(this) and reverts if either balance movedStashVault.sol:626
name()0x06fdde03ERC-20 · viewlib/ERC20.sol:29
symbol()0x95d89b41ERC-20 · viewlib/ERC20.sol:30
decimals()0x313ce567ERC-20 · viewlib/ERC20.sol:31
totalSupply()0x18160dddERC-20 · viewlib/ERC20.sol:32
balanceOf(address)0x70a08231ERC-20 · viewlib/ERC20.sol:33
allowance(address, address)0xdd62ed3eERC-20 · viewlib/ERC20.sol:34
approve(address, uint256)0x095ea7b3ERC-20 · non-viewlib/ERC20.sol:36
transfer(address, uint256)0xa9059cbbERC-20 · non-viewlib/ERC20.sol:43
transferFrom(address, address, uint256)0x23b872ddERC-20 · non-viewlib/ERC20.sol:48

StashVault · events 20

eventtopic0source
Deposit(address indexed caller, address indexed receiver, uint256 assets, uint256 shares)0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7StashVault.sol:126
Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8dbStashVault.sol:127-129
Harvest(address indexed keeper, uint256 rewardHarvested, uint256 feeTaken, uint256 rewardSwapped, uint256 assetsGained, uint256 rateRayAfter)0xeaedd1267621f4a8ee97011faf0e67800ad3063714bd179f078c980bb9a3fa3dStashVault.sol:130-137
AdminTransferStarted(address indexed from, address indexed to)0xe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25cStashVault.sol:138
AdminTransferred(address indexed from, address indexed to)0xf8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6StashVault.sol:139
GuardianSet(address indexed previous, address indexed current)0xc3ce29e3ab42e524b6f6f1b4d3674898d503ee3577a64ac87b555904ebc14138StashVault.sol:140
TreasurySet(address indexed previous, address indexed current)0x21eb548722a564f6e09f039f7aa858ae94c911910f3823b37af2250eeca4f403StashVault.sol:141
KeeperSet(address indexed previous, address indexed current)0x50697e88bd427923002f5d3844f352091c05c0c9c3b69e43830863d222463173StashVault.sol:142
FeeSet(uint256 previousBps, uint256 currentBps)0x74dbbbe280ef27b79a8a0c449d5ae2ba7a31849103241d0f98df70bbc9d03e37StashVault.sol:143
HarvestIntervalSet(uint64 previous, uint64 current)0x29a700bafb6890be297f248b50c1e55964e74a6ac55a93095361acc2ed07e865StashVault.sol:144
VestingPeriodSet(uint64 previous, uint64 current)0xc7e1f46ccf87d1b193018e745d9ca943853fe53abc987aa15ca997d469a102feStashVault.sol:145
DepositsPausedSet(address indexed by, bool paused)0xeb42619f0dc209ac4a718f07efe6bc4199f0782ce8557866622b51a3c2ac725aStashVault.sol:146
HarvestPausedSet(address indexed by, bool paused)0x356bb5f1b5121ae585379248483cbdb71ddc0ef26e6d216374755053c17d1819StashVault.sol:147
RedeemPaused(address indexed by, uint64 until, uint64 cooldownUntil)0xd9e659745047207dca23909d7b05dbec1aedb6e4d56f78e1a2d68ddaff3206d3StashVault.sol:148
RedeemUnpaused(address indexed by)0xf4c6fd9c757a1cb2280e0f7a11cb3d166c85158c6daddb7afcde96e99f0b334bStashVault.sol:149
Rescued(address indexed token, address indexed to, uint256 amount)0x3af790fafda720819b2fc6e15090606e81154e0ac9a92d38ecad006d99d20eccStashVault.sol:150
TwapWindowSet(uint32 previous, uint32 current)0x6e7fd4af29eb9d511c63db3021438423f80f7a488568b2c910bd79e1ec2a65efStashVault.sol:151
MaxSlippageSet(uint256 previousBps, uint256 currentBps)0xda973bd94b10c2214cb2481e0686daab27b8379c7b3da0466b067c532a769dd9StashVault.sol:152
Transfer(address indexed from, address indexed to, uint256 value)0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efinherited, src/lib/IERC20.sol
Approval(address indexed owner, address indexed spender, uint256 value)0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925inherited, src/lib/IERC20.sol

StashVault · errors 35

errorselectorsource
ZeroAddress()0xd92e233dStashVault.sol
ZeroAmount()0x1f2a2005StashVault.sol
ZeroShares()0x9811e0c7StashVault.sol
NotAdmin()0x7bfa4b9fStashVault.sol
NotKeeper()0xf512b278StashVault.sol
NotGuardianOrAdmin()0x11cbee97StashVault.sol
NotPendingAdmin()0x058d9a1bStashVault.sol
AssetIsReward()0xe8e3d7dfStashVault.sol
UnsupportedDecimals()0xd4f1d302StashVault.sol
FeeTooHigh()0xcd4e6167StashVault.sol
IntervalTooLong()0x273aa4ecStashVault.sol
IntervalTooShort()0x4a508f4aStashVault.sol
IntervalAboveVesting()0xf4b1a445StashVault.sol
VestingTooShort()0xeeacd558StashVault.sol
VestingTooLong()0xbf9797feStashVault.sol
DepositsArePaused()0x5a65d188StashVault.sol
HarvestIsPaused()0xe9dd7496StashVault.sol
RedeemIsPaused()0xd67e879fStashVault.sol
HarvestTooSoon()0xcb562bd0StashVault.sol
NothingToHarvest()0x3f29331aStashVault.sol
UnpricedHarvest()0x557bb5afStashVault.sol
SlippageShares(uint256, uint256)0x3e0dcb4aStashVault.sol
SlippageAssets(uint256, uint256)0x806e3e09StashVault.sol
CannotRescueCore()0x68c2cb07StashVault.sol
RedeemPauseTooLong()0xa1699235StashVault.sol
RedeemPauseOnCooldown()0x825b5525StashVault.sol
PoolDoesNotExist()0x9c8787c0StashVault.sol
TokenIsVault()0xf7213580StashVault.sol
TwapWindowOutOfRange()0x1d1e3deeStashVault.sol
SlippageTooHigh()0x850c6f76StashVault.sol
HarvestTooLarge()0x623a14d7StashVault.sol
ERC20ZeroAddress()0xdd1562f6lib/ERC20.sol
ERC20InsufficientBalance()0x590b7c5clib/ERC20.sol
ERC20InsufficientAllowance()0x2fc50d60lib/ERC20.sol
Reentrancy()0xab143c06lib/ReentrancyGuard.sol

StashFactory · functions 9

signatureselectorwhosource
admin()0xf851a440anyoneStashFactory.sol:13
pendingAdmin()0x26782247anyoneStashFactory.sol:14
vaultOf(address)0x0709df45anyoneStashFactory.sol:17
allVaults(uint256)0x9094a91eanyoneStashFactory.sol:18
vaultCount()0xa7c6a100anyoneStashFactory.sol:54
createVault(address, address, address, uint24, address, address, address, address)0x758e91a8admin onlyStashFactory.sol:63-72
retireVault(address)0x61babe44admin onlyStashFactory.sol:101
transferAdmin(address)0x75829defadmin onlyStashFactory.sol:108
acceptAdmin()0x0e18b681**pendingAdmin only**StashFactory.sol:114-115

StashFactory · events 4

eventtopic0source
VaultCreated(address indexed vault, address indexed asset, address indexed reward, address router, uint24 poolFee, address admin, address guardian, address treasury, address keeper)0xc9b8f07d3b928cd39e047a74f436cc619104c1671e1d5ac490df20ab4d96fbb8StashFactory.sol:20-30
VaultRetired(address indexed vault, address indexed asset)0x97bc7c77a17586430e930edc5fb82527e3b48585c1580dfce1f61453978f21eeStashFactory.sol:31
AdminTransferStarted(address indexed from, address indexed to)0xe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25cStashFactory.sol:32
AdminTransferred(address indexed from, address indexed to)0xf8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6StashFactory.sol:33

StashFactory · errors 8

errorselectorsource
NotAdmin()0x7bfa4b9fStashFactory.sol:35
NotPendingAdmin()0x058d9a1bStashFactory.sol:36
ZeroAddress()0xd92e233dStashFactory.sol:37
VaultExists(address existing)0x70743b38StashFactory.sol:38
AssetIsReward()0xe8e3d7dfStashFactory.sol:39
PoolDoesNotExist(address asset, address reward, uint24 fee)0x137b894cStashFactory.sol:40
DuplicateRole()0x86afdf28StashFactory.sol:41
NoVault(address asset)0x36795bf3StashFactory.sol:42

14 · Chain constants

Robinhood Chain, and the contracts a vault leans on.

These addresses come from config/addresses.json, the site's one source of truth, and link to Blockscout. Proof re-reads the chain in your browser: the chain id, that each address has code, and that the router reports this factory.

ChainRobinhood Chain
Chain id—
GasETH
ExplorerBlockscout

Reading config/addresses.json…

WETH/USDG pools

The pools the site measured, with their depth at the block it was read. Depth moves every block; this is a dated snapshot, not a live figure.

Reading config/addresses.json…

15 · Glossary

The words on this page.

Asset
The one coin a vault holds and pays back. asset().
Reward
What the asset pays its holders. Inventory until a harvest sells it. reward().
Share · sCOIN
The vault's ERC-20, named s plus the asset's ticker, with three more decimals than the asset.
Share rate
Coin per whole share, exchangeRateRay() ÷ 1e27. Starts at exactly 1, never falls.
Ray
A fixed-point number with 27 decimals.
Basis point · bps
One hundredth of a percent. 10,000 bps = 100%.
Principal
What the shares have a claim on: totalAssets().
Lock · vesting window
Where a harvest's gain waits, released in a straight line over the window (6 h by default).
Harvest
The keeper's call that sells the reward for more of the asset.
Keeper
The one address allowed to harvest.
Guardian
The address that can pause deposits and harvests, and nothing else.
Admin
The address that sets the vault's settings within their walls.
Treasury
The address the fee is sent to.
TWAP
Time-weighted average price: here, the pool's mean tick over the price window.
Tolerance
How far under the average, after the pool fee, a harvest may fill. maxSlippageBps.
Floor
The least a harvest must add to the vault, or it reverts. harvestFloor().
Virtual shares
1,000 shares and one coin unit that exist only inside the conversions.
Seed deposit
The project's first deposit into a vault, before it opens.
In kind
Paid in the same coin you deposited, not in anything else.
Selector · topic0
The four-byte id of a function or error, and the 32-byte id of an event.