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
Income is pushed into the vault by its harvester. 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.
What is deployed today: one vault, the routed USDG one, forwards deposits into Steakhouse USDG on Morpho Blue and earns there. Every other vault has no harvester set, so nothing is pushed in and its share price stays at exactly 1. A vault with no yield source is a share token and nothing more, which is why the app shows the share price rather than an advertised rate.
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 asset's price risk. A vault holding a tokenized stock is still exposed to that stock.
- It does not make yield a certainty. Most vaults have no yield source wired up at all, and their share price has never moved.
- The routed vault's yield depends on a third party. Deposits there go into Steakhouse USDG on Morpho Blue, so that venue's risk becomes yours.
- 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.
Contracts
| Contract | SOON |
| Token | $SELVO SOON |
Parameters
| Parameter | Value |
|---|---|
| Protocol cut — factory default for a new vault | |
| Protocol cut — routed USDG vault, the default deposit target | read from the vault |
| Hard cap on that cut | |
| Pool fee tiers the deposit route tries | |
| Vaults in the registry | read from the factory |
| Chain | |
| Withdrawal delay | none |
The first two rows differ on purpose: the cut is stored per vault, so the factory default is not automatically what a given vault charges. The figures above are read from the contracts when this page loads; if a read fails they fall back to the values in config.js, which is what the app is configured with.
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
That command produces a factory vault, which is where the 10% default comes from. The routed USDG vault is a different contract and is not created this way.
config.js holds the addresses the front end cannot derive: the factory, the routed vault, the zap and its Uniswap v3 factory, the USDG stablecoin and the tokenized-stock tokens. The vault list itself is not among them — it is read from the registry at the factory address.
Front end
Static HTML, CSS and vanilla JavaScript. config.js is the source of truth for the brand, the chain and the addresses. There is one serverless function, /api/rpc, which proxies JSON-RPC reads to Robinhood Chain: the public node intermittently returns a malformed CORS header that browsers reject, so reads go through the proxy and fall back to the public node directly if it is unreachable. Wallets still talk to the public RPC themselves.
Nothing on these pages invents a number. Where a figure cannot be read, it is left blank or the page says the read failed.