The most common shape in DeFi is two units of account in one contract. Three hundred and four lines of OpenZeppelin's vault, read cold, and every function in it turns out to be a conversion between assets and shares with one decision attached: where the leftover goes.
16 steps~30 min3 nodes for your map
01 · Two units of account
Three hundred and four lines, and the posture changes here. You already own every word this file uses. So read it the way you would read a file nobody prepared for you: imports first, then what it inherits, then what it stores, and only then the functions.
One fact makes the whole file hard, and it is not a keyword. A vault takes one token in and hands back a different token standing for a claim on the pool. Two quantities, assets and shares, and every function below is a conversion between them.
02 · The vault is itself a token
Line 76 reads `is ERC20, IERC4626`. Take the first name literally. This contract is a token, with balances and a total supply of its own, and those balances are the shares. The vault does not point at a share token living somewhere else. It is one.
Line 77 is the statement from checkpoint fourteen, attaching a library's functions to a type so they can be written as if the type owned them. Lines 79 and 80 are the only stored values in the file, and both are `immutable`. Everything else the vault knows, it works out when asked.
03 · Where the totals come from
The constructor takes the underlying token and keeps it on line 108. Line 106 asks that token how many decimals it uses, and line 107 falls back to eighteen when no answer comes back. Line 119 adds an offset on top of it. Line 123 hands the address out.
Then line 128. `totalAssets` is `public view` and its body is a single line, and it is the one reader on this board that keeps nothing of its own. Line 129 is the whole of it. Read what that line actually does before you decide where its number lives.
Line 129 reads `return IERC20(asset()).balanceOf(address(this));`. Where does the number `totalAssets()` hands back come from?
04 · The two directions, named
The board moves to line 132. `convertToShares` takes a number of assets and answers in shares. `convertToAssets` on line 138 does the reverse. One line each, and both hand the work straight down to an internal name with an underscore in front of it.
These two are the plain converters. No caller, no receiver, no transfer, nothing about who is asking. Look at the second argument on lines 134 and 139: both pass `Math.Rounding.Floor`. Hold that word, because four functions further down pass something else.
05 · Two ceilings that are not ceilings
`maxDeposit` and `maxMint` take an address, ignore it, and hand back `type(uint256).max`. That is the largest number a `uint256` holds, so this file imposes no ceiling at all. Both are `virtual`, so a contract inheriting this one can put a real limit there.
The other two are not constants. Line 159 is `balanceOf(owner)`, the getter you read in checkpoint twelve, now reading a share balance. Line 154 runs that answer through `previewRedeem`. The ceiling on a withdrawal is whatever the owner's shares are worth at that moment.
06 · Two words, four places
Four previews, each one a single call to a converter with a direction written into it. Line 164 floors, line 169 ceils, line 174 ceils, line 179 floors. The order is not alphabetical and it is not decorative, and the file never explains it in a comment.
Lines 164 and 174 both call `_convertToShares`, the same internal function declared on line 237, and they differ in exactly one argument. In both of them the caller has already fixed an amount of assets, so the shares are the number being worked out. Decide what that one argument settles before you read further.
07 · The first way in
Line 183 takes an amount of assets and a receiver. Lines 184 to 187 read the ceiling and revert with a custom error carrying three values when the amount is over it. Line 189 turns the assets into shares. Line 190 hands the caller, the receiver and both numbers down to `_deposit`.
Line 192 returns the shares. Four lines of work, and one of them is the entire conversion. Nothing on this board touches a balance or a supply. The public function chooses the two numbers, and something further down the file is what actually moves them.
08 · Four functions, two operations
Line 196 is `mint`, and it is `deposit` read from the other side. The caller names shares rather than assets, line 202 computes what those shares will cost, and line 203 calls the same `_deposit` with the same four arguments in the same order.
That is why four public functions cover two operations. You may fix the amount on either side of the conversion, and the file computes the other side. Lines 209 and 222 are the same pair again for the way out.
09 · The symmetry, in four lines
One line inside each public function, and between them they are the whole arrangement. Each names a preview, each preview names a converter, each converter is handed a direction. Apart from the error they raise, nothing else in the four functions differs.
Read them as two pairs rather than as a list of four, and for each one ask which of the two units the caller typed in and which one the file then had to work out. Tap each in turn, in the order the file writes them.
10 · The line underneath all of them
Function first: a multiplication and a division carried out as one step, so the product is not cut down before it is divided, taking a third argument that says what to do with whatever the division leaves over. The name: mulDiv. The dot in front of it is line 77 doing its work.
So read line 238 as assets times `totalSupply() + 10 ** _decimalsOffset()`, divided by `totalAssets() + 1`. Both of those added terms keep a zero out of the bottom of the fraction, because an empty vault has a supply of zero and line 245 would otherwise divide by it.
11 · Seven assets into a live vault
Take a vault whose `_decimalsOffset()` returns zero on line 302, so `10 ** 0` is one. Its `totalSupply()` is one thousand shares and its `totalAssets()` is two thousand units of the underlying. Somebody calls `deposit(7, ...)` and line 189 sends the seven into `previewDeposit`.
Line 164 passes Floor, so line 238 computes seven times one thousand and one, then divides that product by two thousand and one. Nothing else on the line varies: the offset term is one, and the two totals are the ones just given. Do the arithmetic before you pick.
12 · The same fraction, inverted
Put line 245 beside line 238. `totalAssets() + 1` has moved to the top and `totalSupply() + 10 ** _decimalsOffset()` to the bottom. Same two terms, same `mulDiv`, same rounding argument arriving from whoever called. One expression written twice, once in each direction.
So a vault holding nothing answers one for one, scaled by the offset, and a vault holding twice as many assets as shares answers two assets per share. The ratio is those two totals and nothing else, read at the second somebody asks for it.
13 · Where both ways in arrive
`_deposit` takes four parameters and does two things. Line 259 moves the assets from the caller into this contract. Line 260 mints the shares to the receiver, and `_mint` is the internal function from checkpoint twelve, writing a balance on this very contract.
Line 262 announces both numbers. Every `deposit` and every `mint` funnels through here, the way every transfer, mint and burn in checkpoint twelve funnelled into `_update`. One place where a vault grows, and the public functions only choose the two numbers.
14 · The way out, and the number in the fraction
`_withdraw` has five parameters because a withdrawal can involve a third party: the owner whose shares go, and the caller doing it. Lines 275 to 277 spend an allowance when those two differ, counted in shares, through the ERC20 machinery this contract already carries. Line 285 burns and line 286 transfers out.
Then lines 301 to 303. `_decimalsOffset` is `internal view virtual` and returns zero. That zero is the `10 ** _decimalsOffset()` standing in both conversion lines, so the term inside every conversion this file makes is chosen by whatever inherits it.
15 · The whole file, at once
Every line, comments dimmed. Two immutables, four errors, a constructor, seventeen public functions and seven internal ones. Seventeen sounds like a great many until you notice that eight of them are four operations each named twice, and that two lines of arithmetic sit under all of it.
So be exact about what these three hundred and four lines settle for you, and just as exact about the one thing they cannot tell you, however carefully anybody reads them. The gap between those two is where the chain itself starts.
16 · Shares and assets
Three hundred and four lines, and the difficulty was never the syntax. It was that the file counts in two units at once and never says so in a sentence. Once you know which unit a parameter is in, every function in it reads in one pass.
That is what reading a file cold actually looks like. Not recognising more keywords, but working out what a file is counting before you read what it does with the count. The next one will not announce it either.
BANK_DBowner: the bank
you2,400
what the app is actually showing you
BANK_DBowner: the bank ✍
you2,400their pen
you hold a claim. they hold the pen.
your digital life
BANK · you2,400the bank ✍
INSTAGRAM · you2.1M followersMeta ✍
STEAM · you134 gamesValve ✍
AIRLINE · you58,200 milesthe airline ✍
four tables. zero pens that are yours.
BANK_DBowner: the bank ✍
you2,400
DENIED ✗
try both pens
PLATFORM_DBowner: the platform ✍
her · 8 years2,000,000 followers
one automated decision away
your row stands on all three
FTX_DBowner: FTX ✍
you5 BTC
the row stayed. the backing did not.
CARD_DBowner: your bank ✍
TV you never bought−1,100
fraud reversal+1,100 ✓
someone holds the pen, so someone can fix it
?_DBowner: nobody
youstill yours?
can a table exist that nobody owns?
?_DBowner: ̶n̶o̶b̶o̶d̶y̶
you100
no owner, no pen, no trust?
keeper 1
you100
keeper 2
you100
keeper 3
you100
keeper 4
you100
keeper 5
you100
no THE copy, only copies.
keeper 2
you100
keeper 3
you100
keeper 4
you100
keeper 5
you100
your copy
you100
five copies. one of them is yours.
one attacker, ten thousand faces.
writing costs watts. faking voters buys nothing.
proof of work, burn energy to vote.
rewrite one line, break every lock after it.
the price buys trustlessness. the office already has trust.
ownerless ledger
you?
nobody owns the table. so who owns your row?
Three new nodes on your map
two units · four for two · where it rounds · +10 Lynx