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| Event | rawAssets()coin held | totalAssets()coin counted | totalSupply()shares | Share 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 |
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 forkrawAssets() 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.
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
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- 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.
- RAY = 1027: the rate's own precision.
- totalAssets(): the coin the vault counts, its balance minus the part still vesting.
- One virtual unit of coin, so an empty vault is never a division by zero.
- totalSupply(): shares outstanding.
- 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
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 = 0Approve 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.
Deposits paused? The guardian or the admin can pause them.
:371DepositsArePaused()0x5a65d188Receiver is the zero address?
:372ZeroAddress()0xd92e233dZero assets?
:373ZeroAmount()0x1f2a2005Pull the coin. totalAssets() is read before and after transferFrom.
:375-378transferFrom
Measure what arrived. A coin with a transfer tax lands short; shares are minted on what landed, not on what was asked.
:382-383ZeroAmount()0x1f2a2005Price the shares. received × (supply + 1,000) ÷ (assetsBefore + 1), 512-bit, rounded down.
:387-388ZeroShares()0x9811e0c7Fewer shares than you asked for? Your floor, minSharesOut.
:389SlippageShares(got, wanted)0x3e0dcb4aMint the shares to the receiver and emit Deposit.
:391-392mint · 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 sharesThe 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.
Caller is not the keeper?
:442NotKeeper()0xf512b278Harvests paused? Guardian or admin.
:443HarvestIsPaused()0xe9dd7496Too soon? The first harvest is free; after that, one per harvestInterval.
:445-446HarvestTooSoon()0xcb562bd0No reward to sell?
:448-449NothingToHarvest()0x3f29331aTake the fee in the reward token: harvested × feeBps ÷ 10,000 to the treasury. Never from principal.
:451-452fee → treasury
Measure what is left to sell (the reward may be taxed too).
:455-456NothingToHarvest()0x3f29331aPrice 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-463UnpricedHarvest()0x557bb5afSwap: approve the router for exactly the amount, exactInputSingle reward → asset with amountOutMinimum 0, then set the allowance back to 0.
:467-482swap · allowance 0
Gained less than the floor? Checked on the vault's own measured gain, not on the router's figure.
:485-486SlippageAssets(got, wanted)0x806e3e09Lock the gain, record lastHarvest and emit Harvest. Set last, so a reverted harvest does not use up the interval.
:490-505lock · 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 daysWhat 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.
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 fee | poolFee | Floor ÷ 30-min quote | Band under the average |
|---|---|---|---|
| 0.01% | 100 | 0.989901 | 1.0099% |
| 0.05% | 500 | 0.989505 | 1.0495% |
| 0.3% | 3000 | 0.987030 | 1.2970% |
| 1% | 10000 | 0.980100 | 1.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| After the harvest | Still locked | Counted | Share rate |
|---|---|---|---|
| 0 h | 2,474.511185 | 10,000.000000 | 1.000000000 |
| 1 h | 2,062.092655 | 10,412.418530 | 1.041241852 |
| 2 h | 1,649.674124 | 10,824.837061 | 1.082483706 |
| 3 h | 1,237.255593 | 11,237.255592 | 1.123725559 |
| 4 h | 824.837062 | 11,649.674123 | 1.164967412 |
| 5 h | 412.418531 | 12,062.092654 | 1.206209265 |
| 6 h | 0.000000 | 12,474.511185 | 1.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- 1Harvest 1 locks its gain; the lock falls in a straight line towards its end.
- 2Harvest 2 lands early: what is still locked is carried over, not released.
- 3The new gain is added on top, and the combined lock starts falling from now.
- 4Where the first lock would have ended (dashed: the path it no longer takes).
- 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 = 0In 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).
Redemption paused? Only the admin can pause it, for 72 hours at most.
:407RedeemIsPaused()0xd67e879fReceiver is the zero address?
:408ZeroAddress()0xd92e233dZero shares?
:409ZeroAmount()0x1f2a2005Redeeming someone else's shares? Their allowance is spent, unless it is unlimited.
:411-417ERC20InsufficientAllowance()0x2fc50d60Price 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-420convert
Clamp to what is counted. Defensive: a 256-run fuzz shows it never engages.
:423clamp
Nothing to pay, or less than your floor?
:424-425SlippageAssets(got, wanted)0x806e3e09Burn the shares, send the coin and emit Withdraw.
:427-430burn · 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.
Admin
one per vaultCan
- 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
Guardian
brakes onlyCan
- pause and unpause deposits
- pause and unpause harvests
Cannot
- pause redemption, ever
- change any setting or move any token
Keeper
the only harvesterCan
- 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
Treasury
receives the feeGets
- the fee, in the reward token, at each harvest
Cannot
- call anything the public cannot; it is an address, not a power
Factory admin
creates, then steps awayCan
- 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-75Percentages · 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
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.
| Switch | Who | Limit | While it is on |
|---|---|---|---|
| Deposits | guardian or admin | none: on until switched off | maxDeposit returns 0; deposit reverts DepositsArePaused |
| Harvests | guardian or admin | none: on until switched off | payouts wait in the vault; the rate stays where the lock leaves it |
| Redemption | admin only | 72 h at most, then a 72 h lockout | maxRedeem returns 0; redeemPaused() is true; redeem reverts RedeemIsPaused |
The longest a redemption pause can last, and what follows it
MAX_REDEEM_PAUSE · REDEEM_PAUSE_COOLDOWNA 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.
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-4626 | Here | Why |
|---|---|---|
| mint(shares) | absent | an exact share count cannot be promised when the coin arrives short |
| withdraw(assets) | absent | same reason, the other way round |
| deposit(assets, receiver) | kept | calls the three-argument form with minSharesOut = 0 |
| redeem(shares, receiver, owner) | kept | calls the four-argument form with minAssetsOut = 0 |
| floors on both | added | minSharesOut and minAssetsOut: revert instead of paying less |
| previewDeposit | upper bound | a taxed coin mints fewer shares than previewed |
| maxDeposit(address) | ignores the argument | 0 while deposits are paused, otherwise unlimited |
| maxRedeem(owner) | pause-aware | 0 during a redemption pause, otherwise the owner's balance |
| totalAssets() | net of the lock | excludes the part still vesting and all reward inventory |
| decimals() | asset + 3 | one share reads as about one coin; the extra digits absorb rounding |
| previewMint, previewWithdraw, maxMint, maxWithdraw | absent | they 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
| signature | selector | who / type | source |
|---|---|---|---|
RAY() | 0x552033c4 | constant | StashVault.sol:31 |
BPS() | 0x249d39e9 | constant | StashVault.sol:32 |
DECIMALS_OFFSET() | 0xaea70acc | constant | StashVault.sol:36 |
MIN_ASSET_DECIMALS() | 0xdd7e15a7 | constant | StashVault.sol:39 |
VIRTUAL_SHARES() | 0x88c47f68 | constant | StashVault.sol:44 |
MAX_FEE_BPS() | 0xd55be8c6 | constant | StashVault.sol:46 |
DEFAULT_FEE_BPS() | 0x33d8262c | constant | StashVault.sol:47 |
DEFAULT_HARVEST_INTERVAL() | 0xd57d4ad5 | constant | StashVault.sol:48 |
MAX_HARVEST_INTERVAL() | 0x0d15e044 | constant | StashVault.sol:49 |
MIN_HARVEST_INTERVAL() | 0xe8343a66 | constant | StashVault.sol:52 |
DEFAULT_VESTING_PERIOD() | 0x098480ec | constant | StashVault.sol:58 |
MIN_VESTING_PERIOD() | 0xc85fcbe8 | constant | StashVault.sol:59 |
MAX_VESTING_PERIOD() | 0xe5a5e674 | constant | StashVault.sol:60 |
MAX_REDEEM_PAUSE() | 0xe34f7145 | constant | StashVault.sol:61 |
REDEEM_PAUSE_COOLDOWN() | 0xa0c62586 | constant | StashVault.sol:62 |
DEFAULT_TWAP_WINDOW() | 0x5fcb2120 | constant | StashVault.sol:68 |
MIN_TWAP_WINDOW() | 0x1a65893b | constant | StashVault.sol:69 |
MAX_TWAP_WINDOW() | 0xb88a92b4 | constant | StashVault.sol:70 |
DEFAULT_MAX_SLIPPAGE_BPS() | 0xcdb994e9 | constant | StashVault.sol:74 |
SLIPPAGE_CAP_BPS() | 0xf39fdeb1 | constant | StashVault.sol:75 |
asset() | 0x38d52e0f | immutable · address (IERC20) | StashVault.sol:82 |
reward() | 0x228cb733 | immutable · address (IERC20) | StashVault.sol:84 |
router() | 0xf887ea40 | immutable · address (ISwapRouter02) | StashVault.sol:86 |
poolFee() | 0x089fe6aa | immutable · uint24 | StashVault.sol:88 |
pool() | 0x16f0115b | immutable · address | StashVault.sol:91 |
assetDecimals() | 0xc2d41601 | immutable · uint8 | StashVault.sol:92 |
admin() | 0xf851a440 | storage · address | StashVault.sol:96 |
pendingAdmin() | 0x26782247 | storage · address | StashVault.sol:97 |
guardian() | 0x452a9320 | storage · address | StashVault.sol:98 |
treasury() | 0x61d027b3 | storage · address | StashVault.sol:99 |
keeper() | 0xaced1661 | storage · address | StashVault.sol:100 |
feeBps() | 0x24a9d853 | storage · uint256 | StashVault.sol:102 |
harvestInterval() | 0x14dc0ce0 | storage · uint64 | StashVault.sol:103 |
lastHarvest() | 0xf1a392da | storage · uint64 | StashVault.sol:104 |
vestingPeriod() | 0x7313ee5a | storage · uint64 | StashVault.sol:108 |
lockedProfit() | 0x44b81396 | storage · uint256 | StashVault.sol:110 |
lockedProfitAt() | 0xbc028917 | storage · uint64 | StashVault.sol:111 |
lockedProfitUntil() | 0x049a4041 | storage · uint64 | StashVault.sol:114 |
depositsPaused() | 0x60da3e83 | storage · bool | StashVault.sol:116 |
harvestPaused() | 0x8a3cf676 | storage · bool | StashVault.sol:117 |
redeemPausedUntil() | 0xa8378459 | storage · uint64 | StashVault.sol:118 |
redeemPauseCooldownUntil() | 0xfd61a56f | storage · uint64 | StashVault.sol:119 |
twapWindow() | 0x8107e133 | storage · uint32 | StashVault.sol:121 |
maxSlippageBps() | 0xc4aa7395 | storage · uint256 | StashVault.sol:122 |
rawAssets() | 0x4115e031 | view · public view | StashVault.sol:258 |
lockedProfitRemaining() | 0x0dc3f9cd | view · public view | StashVault.sol:264 |
totalAssets() | 0x01e1d114 | view · public view | StashVault.sol:291 |
convertToShares(uint256) | 0xc6e6f592 | view · public view | StashVault.sol:299 |
convertToAssets(uint256) | 0x07a2d13a | view · public view | StashVault.sol:303 |
previewDeposit(uint256) | 0xef8b30f7 | view · external view | StashVault.sol:309 |
previewRedeem(uint256) | 0x4cdad506 | view · external view | StashVault.sol:313 |
maxDeposit(address) | 0x402d267d | view · external view | StashVault.sol:317 |
maxRedeem(address) | 0xd905777e | view · external view | StashVault.sol:321 |
exchangeRateRay() | 0x54b3b33c | view · public view | StashVault.sol:331 |
redeemPaused() | 0xb235d468 | view · external view | StashVault.sol:335 |
harvestReady() | 0x779693fa | view · external view | StashVault.sol:339 |
harvestFloor(uint256) | 0xf96054b8 | view · public view | StashVault.sol:349 |
deposit(uint256, address) | 0x6e553f65 | deposit · redeem · harvest · anyone | StashVault.sol:359 |
deposit(uint256, address, uint256) | 0xbc157ac1 | deposit · redeem · harvest · anyone | StashVault.sol:366 |
redeem(uint256, address, address) | 0xba087652 | deposit · redeem · harvest · anyone (owner or approved) | StashVault.sol:397 |
redeem(uint256, address, address, uint256) | 0x9f40a7b3 | deposit · redeem · harvest · anyone (owner or approved; spends allowance if msg.sender != owner) | StashVault.sol:402 |
harvest(uint256) | 0xddc63262 | deposit · redeem · harvest · **keeper only** — if (msg.sender != keeper) revert NotKeeper(); | StashVault.sol:441-442 |
transferAdmin(address) | 0x75829def | admin · onlyAdmin | StashVault.sol:510 |
acceptAdmin() | 0x0e18b681 | admin · **pendingAdmin only** — if (msg.sender != pendingAdmin) revert NotPendingAdmin(); | StashVault.sol:516-517 |
setGuardian(address) | 0x8a0dac4a | admin · onlyAdmin | StashVault.sol:524 |
setTreasury(address) | 0xf0f44260 | admin · onlyAdmin | StashVault.sol:530 |
setKeeper(address) | 0x748747e6 | admin · onlyAdmin | StashVault.sol:536 |
setFeeBps(uint256) | 0x72c27b62 | admin · onlyAdmin (reverts FeeTooHigh above MAX_FEE_BPS) | StashVault.sol:542 |
setHarvestInterval(uint64) | 0x40baf51d | admin · onlyAdmin | StashVault.sol:548 |
setVestingPeriod(uint64) | 0xf4768256 | admin · onlyAdmin | StashVault.sol:562 |
setTwapWindow(uint32) | 0x5f553545 | admin · onlyAdmin | StashVault.sol:571 |
setMaxSlippageBps(uint256) | 0x25fc1b3d | admin · onlyAdmin | StashVault.sol:579 |
setDepositsPaused(bool) | 0x1c481e2d | admin · onlyGuardianOrAdmin | StashVault.sol:587 |
setHarvestPaused(bool) | 0x40a02887 | admin · onlyGuardianOrAdmin | StashVault.sol:592 |
pauseRedeem(uint64) | 0x4efc7530 | admin · onlyAdmin — **guardian has no say** (line 597-598 comment) | StashVault.sol:599 |
unpauseRedeem() | 0xaf3345d1 | admin · onlyAdmin | StashVault.sol:612 |
rescue(address, address, uint256) | 0x20ff430b | admin · onlyAdmin; refuses asset/reward/address(this) and reverts if either balance moved | StashVault.sol:626 |
name() | 0x06fdde03 | ERC-20 · view | lib/ERC20.sol:29 |
symbol() | 0x95d89b41 | ERC-20 · view | lib/ERC20.sol:30 |
decimals() | 0x313ce567 | ERC-20 · view | lib/ERC20.sol:31 |
totalSupply() | 0x18160ddd | ERC-20 · view | lib/ERC20.sol:32 |
balanceOf(address) | 0x70a08231 | ERC-20 · view | lib/ERC20.sol:33 |
allowance(address, address) | 0xdd62ed3e | ERC-20 · view | lib/ERC20.sol:34 |
approve(address, uint256) | 0x095ea7b3 | ERC-20 · non-view | lib/ERC20.sol:36 |
transfer(address, uint256) | 0xa9059cbb | ERC-20 · non-view | lib/ERC20.sol:43 |
transferFrom(address, address, uint256) | 0x23b872dd | ERC-20 · non-view | lib/ERC20.sol:48 |
StashVault · events 20
| event | topic0 | source |
|---|---|---|
Deposit(address indexed caller, address indexed receiver, uint256 assets, uint256 shares) | 0xdcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7 | StashVault.sol:126 |
Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares) | 0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db | StashVault.sol:127-129 |
Harvest(address indexed keeper, uint256 rewardHarvested, uint256 feeTaken, uint256 rewardSwapped, uint256 assetsGained, uint256 rateRayAfter) | 0xeaedd1267621f4a8ee97011faf0e67800ad3063714bd179f078c980bb9a3fa3d | StashVault.sol:130-137 |
AdminTransferStarted(address indexed from, address indexed to) | 0xe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25c | StashVault.sol:138 |
AdminTransferred(address indexed from, address indexed to) | 0xf8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6 | StashVault.sol:139 |
GuardianSet(address indexed previous, address indexed current) | 0xc3ce29e3ab42e524b6f6f1b4d3674898d503ee3577a64ac87b555904ebc14138 | StashVault.sol:140 |
TreasurySet(address indexed previous, address indexed current) | 0x21eb548722a564f6e09f039f7aa858ae94c911910f3823b37af2250eeca4f403 | StashVault.sol:141 |
KeeperSet(address indexed previous, address indexed current) | 0x50697e88bd427923002f5d3844f352091c05c0c9c3b69e43830863d222463173 | StashVault.sol:142 |
FeeSet(uint256 previousBps, uint256 currentBps) | 0x74dbbbe280ef27b79a8a0c449d5ae2ba7a31849103241d0f98df70bbc9d03e37 | StashVault.sol:143 |
HarvestIntervalSet(uint64 previous, uint64 current) | 0x29a700bafb6890be297f248b50c1e55964e74a6ac55a93095361acc2ed07e865 | StashVault.sol:144 |
VestingPeriodSet(uint64 previous, uint64 current) | 0xc7e1f46ccf87d1b193018e745d9ca943853fe53abc987aa15ca997d469a102fe | StashVault.sol:145 |
DepositsPausedSet(address indexed by, bool paused) | 0xeb42619f0dc209ac4a718f07efe6bc4199f0782ce8557866622b51a3c2ac725a | StashVault.sol:146 |
HarvestPausedSet(address indexed by, bool paused) | 0x356bb5f1b5121ae585379248483cbdb71ddc0ef26e6d216374755053c17d1819 | StashVault.sol:147 |
RedeemPaused(address indexed by, uint64 until, uint64 cooldownUntil) | 0xd9e659745047207dca23909d7b05dbec1aedb6e4d56f78e1a2d68ddaff3206d3 | StashVault.sol:148 |
RedeemUnpaused(address indexed by) | 0xf4c6fd9c757a1cb2280e0f7a11cb3d166c85158c6daddb7afcde96e99f0b334b | StashVault.sol:149 |
Rescued(address indexed token, address indexed to, uint256 amount) | 0x3af790fafda720819b2fc6e15090606e81154e0ac9a92d38ecad006d99d20ecc | StashVault.sol:150 |
TwapWindowSet(uint32 previous, uint32 current) | 0x6e7fd4af29eb9d511c63db3021438423f80f7a488568b2c910bd79e1ec2a65ef | StashVault.sol:151 |
MaxSlippageSet(uint256 previousBps, uint256 currentBps) | 0xda973bd94b10c2214cb2481e0686daab27b8379c7b3da0466b067c532a769dd9 | StashVault.sol:152 |
Transfer(address indexed from, address indexed to, uint256 value) | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef | inherited, src/lib/IERC20.sol |
Approval(address indexed owner, address indexed spender, uint256 value) | 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 | inherited, src/lib/IERC20.sol |
StashVault · errors 35
| error | selector | source |
|---|---|---|
ZeroAddress() | 0xd92e233d | StashVault.sol |
ZeroAmount() | 0x1f2a2005 | StashVault.sol |
ZeroShares() | 0x9811e0c7 | StashVault.sol |
NotAdmin() | 0x7bfa4b9f | StashVault.sol |
NotKeeper() | 0xf512b278 | StashVault.sol |
NotGuardianOrAdmin() | 0x11cbee97 | StashVault.sol |
NotPendingAdmin() | 0x058d9a1b | StashVault.sol |
AssetIsReward() | 0xe8e3d7df | StashVault.sol |
UnsupportedDecimals() | 0xd4f1d302 | StashVault.sol |
FeeTooHigh() | 0xcd4e6167 | StashVault.sol |
IntervalTooLong() | 0x273aa4ec | StashVault.sol |
IntervalTooShort() | 0x4a508f4a | StashVault.sol |
IntervalAboveVesting() | 0xf4b1a445 | StashVault.sol |
VestingTooShort() | 0xeeacd558 | StashVault.sol |
VestingTooLong() | 0xbf9797fe | StashVault.sol |
DepositsArePaused() | 0x5a65d188 | StashVault.sol |
HarvestIsPaused() | 0xe9dd7496 | StashVault.sol |
RedeemIsPaused() | 0xd67e879f | StashVault.sol |
HarvestTooSoon() | 0xcb562bd0 | StashVault.sol |
NothingToHarvest() | 0x3f29331a | StashVault.sol |
UnpricedHarvest() | 0x557bb5af | StashVault.sol |
SlippageShares(uint256, uint256) | 0x3e0dcb4a | StashVault.sol |
SlippageAssets(uint256, uint256) | 0x806e3e09 | StashVault.sol |
CannotRescueCore() | 0x68c2cb07 | StashVault.sol |
RedeemPauseTooLong() | 0xa1699235 | StashVault.sol |
RedeemPauseOnCooldown() | 0x825b5525 | StashVault.sol |
PoolDoesNotExist() | 0x9c8787c0 | StashVault.sol |
TokenIsVault() | 0xf7213580 | StashVault.sol |
TwapWindowOutOfRange() | 0x1d1e3dee | StashVault.sol |
SlippageTooHigh() | 0x850c6f76 | StashVault.sol |
HarvestTooLarge() | 0x623a14d7 | StashVault.sol |
ERC20ZeroAddress() | 0xdd1562f6 | lib/ERC20.sol |
ERC20InsufficientBalance() | 0x590b7c5c | lib/ERC20.sol |
ERC20InsufficientAllowance() | 0x2fc50d60 | lib/ERC20.sol |
Reentrancy() | 0xab143c06 | lib/ReentrancyGuard.sol |
StashFactory · functions 9
| signature | selector | who | source |
|---|---|---|---|
admin() | 0xf851a440 | anyone | StashFactory.sol:13 |
pendingAdmin() | 0x26782247 | anyone | StashFactory.sol:14 |
vaultOf(address) | 0x0709df45 | anyone | StashFactory.sol:17 |
allVaults(uint256) | 0x9094a91e | anyone | StashFactory.sol:18 |
vaultCount() | 0xa7c6a100 | anyone | StashFactory.sol:54 |
createVault(address, address, address, uint24, address, address, address, address) | 0x758e91a8 | admin only | StashFactory.sol:63-72 |
retireVault(address) | 0x61babe44 | admin only | StashFactory.sol:101 |
transferAdmin(address) | 0x75829def | admin only | StashFactory.sol:108 |
acceptAdmin() | 0x0e18b681 | **pendingAdmin only** | StashFactory.sol:114-115 |
StashFactory · events 4
| event | topic0 | source |
|---|---|---|
VaultCreated(address indexed vault, address indexed asset, address indexed reward, address router, uint24 poolFee, address admin, address guardian, address treasury, address keeper) | 0xc9b8f07d3b928cd39e047a74f436cc619104c1671e1d5ac490df20ab4d96fbb8 | StashFactory.sol:20-30 |
VaultRetired(address indexed vault, address indexed asset) | 0x97bc7c77a17586430e930edc5fb82527e3b48585c1580dfce1f61453978f21ee | StashFactory.sol:31 |
AdminTransferStarted(address indexed from, address indexed to) | 0xe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25c | StashFactory.sol:32 |
AdminTransferred(address indexed from, address indexed to) | 0xf8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6 | StashFactory.sol:33 |
StashFactory · errors 8
| error | selector | source |
|---|---|---|
NotAdmin() | 0x7bfa4b9f | StashFactory.sol:35 |
NotPendingAdmin() | 0x058d9a1b | StashFactory.sol:36 |
ZeroAddress() | 0xd92e233d | StashFactory.sol:37 |
VaultExists(address existing) | 0x70743b38 | StashFactory.sol:38 |
AssetIsReward() | 0xe8e3d7df | StashFactory.sol:39 |
PoolDoesNotExist(address asset, address reward, uint24 fee) | 0x137b894c | StashFactory.sol:40 |
DuplicateRole() | 0x86afdf28 | StashFactory.sol:41 |
NoVault(address asset) | 0x36795bf3 | StashFactory.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.
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.