Docs
What the protocol does, what the contracts guarantee, and what is actually deployed. Written to be checkable against the code, not to sell anything.
YieldShares
A vault holds one asset. You deposit that asset and the vault mints you a share token. The share token is an ordinary ERC-20: it has a symbol, it shows in your wallet, and it transfers without the protocol's permission.
The share price is:
pricePerShare = (totalAssets + 1) / (totalSupply + 1)
The + 1 on each side is a virtual asset and a virtual share. It exists so that the very first deposit cannot be used to manipulate the price for the second one. Without it, an attacker deposits 1 wei, donates a large amount to the vault, and the next depositor's shares round down to zero.
Yield
Fee income is pushed into the vault by the harvester (in production, the pool hook that collects swap fees). The vault takes the protocol's cut, adds the rest to totalAssets, and mints no new shares. The supply is unchanged and the assets grew, so every share is now worth more.
There is nothing to claim. If you transfer the share, the accrued yield transfers with it, because it lives in the redemption price rather than in a per-user reward balance.
Redemption
Burn shares, receive the underlying at the current price. No lockup, no queue, no epoch. Rounding on withdrawals and mints always goes in the vault's favour, by at most a few wei, so a sequence of deposits and redemptions cannot drain other holders.
What the contract guarantees
| Property | How it is enforced |
|---|---|
| A donation cannot move the share price | totalAssets is a storage variable, not balanceOf(this). Raw transfers into the vault are invisible to pricing. |
| The first depositor cannot round out the second | Virtual asset and virtual share in every conversion. |
| The protocol fee has a ceiling | MAX_FEE_BPS is a constant checked in the constructor and in setFee. The owner cannot exceed it. |
| The owner cannot take shareholders' assets | skim can only move the balance above totalAssets. The backing is out of reach. |
| Only the harvester can add yield | harvest checks msg.sender == harvester and pulls the tokens from the caller. |
| Fee-on-transfer assets are rejected | Deposits verify the received amount matches the requested amount, and revert otherwise. |
What it does not guarantee
- It does not protect you from the underlying position's risk. Impermanent loss and price risk are unchanged.
- It does not make yield a certainty. If no fees are harvested, the share price does not move.
- It has not been audited. The tests cover the properties above, but tests are not a review.
- The vaults are new and start empty. Being on chain is not the same as being battle-tested.
- The owner controls the harvester address and the fee within the cap. That is a trust assumption until ownership moves to a timelock.
Private execution
The intended design: you sign an intent describing the outcome you want rather than a swap call. Solvers compete to fill it and one of them submits the transaction. Your order is never a pending transaction anyone can read and sandwich, and because the solver pays gas, the fee can be settled in the traded asset.
Vaults shipped first. Routing is designed and not yet live, and the app does not offer it.
Prices
Lending, take-profit rules and anything else that has to act on a number needs a price that a single trade cannot set. Aurex takes one from Uniswap v3, averaged over thirty minutes, through UniV3TwapOracle. A feed is created by calling OracleFactory.create, which anybody can do, for any listed asset, without asking us. Nobody can remove one afterwards, including us.
The factory refuses pools that cannot carry an average. The test people reach for first is whether the pool answers observe() for the window, and on this chain that test passes on nearly every pool, including ones that have recorded a single trade in their whole life. Those are the worst of the lot: with one stored observation the newest is also the oldest, so anyone can push the price, wait out the window, and the average becomes the number they pushed it to. The call succeeding proves nothing.
So the factory counts the trades a pool has actually recorded, which is not the same as the slots it has paid for. Buying a pool more slots is one permissionless call costing about a dollar, and the next trade promotes the pool's slot count to the new size while it still holds two real observations. Counting slots would wave that pool straight through.
Depth is the other half. An average is only as good as the cost of moving it, so a pool under the floor is refused whatever its history looks like.
None of those last two bind forever. Liquidity can leave and trading can stop the block after a feed is created, which is why OracleHealth is a view anybody can call at any time, and why the prices page reads it live on every load instead of showing what was true at deployment.
Contracts
| Contract | Purpose | Address |
|---|---|---|
VaultFactory | Deploys and registers vaults. The app's entire market list is one read against this. | not deployed |
YieldShares | One per asset. ERC-4626 accounting with an ERC-20 share token. | 16 live, listed in the app |
OracleFactory | Builds a time-weighted price feed for any asset whose pool can carry one. No owner, no allowlist, no fee. | not deployed |
OracleHealth | Says whether a pool can still carry an honest average. A view, callable by anyone, forever. | not deployed |
UniV3TwapOracle | One per feed. Reads a thirty minute average and reports it the way Morpho expects. | listed on prices |
Parameters
| Parameter | Value |
|---|---|
| Factory vaults: cut of harvested fee income | |
| Routed USDG vault: cut of the gain | 2% of yield only, never principal |
| Can either fee touch your deposit? | No. Both apply to earnings only. |
| Can withdrawals be paused or blocked? | No. There is no pause function and no owner check on withdraw or redeem. |
| Hard cap on that cut | |
| Pool fee tiers | |
| Chain | |
| Withdrawal delay | none |
| Price window | 30 minutes |
| Pool depth a feed requires | |
| Trades a pool must have recorded |
Running it yourself
Tests:
forge test
Deploy the factory, then create a vault per asset:
forge create contracts/VaultFactory.sol:VaultFactory \
--rpc-url $RPC --private-key $KEY \
--constructor-args 1000 $TREASURY
cast send $FACTORY "createVault(address,string,string)" \
$ASSET "YieldShares NVDA" "ys-NVDA" \
--rpc-url $RPC --private-key $KEY
The market list is read entirely from the registries at vaults.factory and vaults.legacyFactory, so a new vault appears without a deploy. config.js additionally holds the vaults deployed outside the factory (the routed one and the retired one), the stablecoin, the zap, the Uniswap v3 factory and the tokenized-asset list. api/vaults.js carries the factory and routed addresses a second time because a serverless function cannot import a file that assigns a browser global; check.mjs fails the build if the two copies drift.
Front end
Static HTML, CSS and vanilla JavaScript. config.js is the only source of truth for the brand, the chain and the addresses. There are four serverless functions: /api/vaults (the whole vault list, read server-side and cached), /api/rpc (a JSON-RPC proxy with node failover), /api/health (a real chain read, for uptime checks) and /api/prices, which proxies public stock quotes; a symbol with no quote is omitted rather than filled with a placeholder.