@paydprotocol/mcp 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/server.js +466 -72
- package/docs/HOW_IT_WORKS.md +351 -0
- package/docs/SDK.md +323 -0
- package/docs/llms.txt +36 -0
- package/package.json +3 -2
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# HOW_IT_WORKS.md — the whole system, once through
|
|
2
|
+
|
|
3
|
+
`README.md` says what a holder gets. This says how it is built, in one pass, for
|
|
4
|
+
someone who intends to read the code afterwards. `ARCHITECTURE.md` holds the
|
|
5
|
+
reasoning behind every choice mentioned here and is not a prerequisite.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The mechanism in one paragraph
|
|
10
|
+
|
|
11
|
+
A token is launched on **Pons v2** with a contract — not a wallet — as its
|
|
12
|
+
creator-fee recipient. Every trade pays the curve fee plus **the tax that
|
|
13
|
+
launch's creator set**, and most of it reaches that contract — the tax in full,
|
|
14
|
+
plus the share of the curve fee Pons does not keep. **There is no single rate**:
|
|
15
|
+
it is chosen per launch, `FeeVault.economics()` derives it rather than storing
|
|
16
|
+
it, and any figure written here would be true of one token and false of the
|
|
17
|
+
rest. The contract converts what it collects into **tokenised stocks** on
|
|
18
|
+
Uniswap v3, the whole basket in one purchase, and hands them to a second
|
|
19
|
+
contract that distributes them to the token's holders **pro rata to what they
|
|
20
|
+
held**, with no staking and no action on their part. Who held what is computed
|
|
21
|
+
off-chain, committed on-chain as a Merkle root, and re-computable by anyone from
|
|
22
|
+
the chain alone.
|
|
23
|
+
|
|
24
|
+
That pair of contracts is no longer made once, for one token: a **`Payd`**
|
|
25
|
+
registry mints a fresh pair per launch through its **`DistributionFactory`**, and the platform's own token, `$PAYD`, is one of
|
|
26
|
+
its tenants rather than its landlord.
|
|
27
|
+
|
|
28
|
+
## Why any of it is off-chain
|
|
29
|
+
|
|
30
|
+
The token is minted by the Pons factory. It has **no transfer hook**, so no
|
|
31
|
+
contract can know a past balance. Without staking — and staking is a click, a
|
|
32
|
+
lock-up, and a contract holding your tokens — the split has to be computed off
|
|
33
|
+
the chain and committed to it.
|
|
34
|
+
|
|
35
|
+
That is the whole trust surface, and it is worth stating plainly rather than
|
|
36
|
+
burying: **one key publishes the roots.** What it cannot do is invent money, pay
|
|
37
|
+
someone twice, take back what was paid, or move funds anywhere — those are
|
|
38
|
+
contract-level guarantees, not promises. What it could do is misdirect what has
|
|
39
|
+
not been delivered yet, a figure the contract publishes as
|
|
40
|
+
`Distributor.quoteAtRisk()` and which continuous delivery keeps at roughly one
|
|
41
|
+
epoch. [§S29](ARCHITECTURE.md#s29--the-keeper-publishes-and-the-root-takes-effect-immediately)
|
|
42
|
+
argues the alternative — a bond and a challenge window — and says why it was
|
|
43
|
+
dropped.
|
|
44
|
+
|
|
45
|
+
## The eight contracts
|
|
46
|
+
|
|
47
|
+
**Per launch**, one of each:
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
| :--- | :--- |
|
|
51
|
+
| **`FeeVault`** | Receives the creator fees in the launch's currency. Splits them three ways, buys the whole basket, sends the stocks straight to the Distributor. Holds no stock, ever. |
|
|
52
|
+
| **`Distributor`** | Holds the stocks and pays them out against a Merkle proof. Remembers what each holder has already been paid. |
|
|
53
|
+
|
|
54
|
+
**Once, for the platform:**
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
| :--- | :--- |
|
|
58
|
+
| **`Payd`** | The registry, and **an interface over the factories** rather than a pointer to one. Holds the parameters a creator may not choose alone: which stocks a basket may contain, which currencies a launch may be quoted in, what the platform takes — and which factories may build here at all. **It launches nothing** — the creator launches their own token on Pons, so the registry is the Pons `deployer` of nothing and holds no standing power over any launch. |
|
|
59
|
+
| **`DistributionFactory`** | Makes the pairs, and only that. Split out of `Payd` because a contract that does `new FeeVault()` carries that creation code: `Payd` weighed **54,635 bytes of initcode**, above the EIP-3860 ceiling, and 18,704 once the machinery left. It is also what makes a new vault implementation ordinary — a new factory, admitted by `Payd` under two keys and 48 h, and the vaults that follow are born in **this** registry. **One factory declares one payout mode** (`MODE`, `"distribution"` here): several may be admitted at once, and a vault is stamped with its own at birth. ([§S46](ARCHITECTURE.md#s46--one-payout-mode-is-one-factory-and-the-stamp-that-keeps-them-apart)) |
|
|
60
|
+
| **`Treasury`** | Receives the platform's share of every launch and splits it four ways: dev ⅓, `$PAYD` rewards ⅓, buy-and-burn ⅙, LP ⅙. No gas refund anywhere in it — the platform has every reason to call its own pockets. |
|
|
61
|
+
| **`Collector`** | Settles N launches in one transaction. A router that holds nothing: the stocks go straight to the holder, the refunded gas straight to the caller. ([§S39](ARCHITECTURE.md#s39--collector-settling-n-launches-in-one-transaction-and-the-door-it-uses)) |
|
|
62
|
+
| **`Timelock`** | 48 h, OpenZeppelin, self-administered. Its powers are listed below, and none of them moves value. |
|
|
63
|
+
| **`Bootstrap`** | Ties the knot: `FeeVault` and `Distributor` each need the other's address, so neither can be deployed first. The factory deploys one per `create` and abandons it in the same transaction — no `Bootstrap` is ever a standing contract. |
|
|
64
|
+
|
|
65
|
+
There is **no owner** anywhere. No `withdraw` exists for any privileged address;
|
|
66
|
+
the only `withdraw()` on either contract pays the caller a payment that had
|
|
67
|
+
already failed to reach that same caller, takes no address argument, and can move
|
|
68
|
+
nobody else's balance.
|
|
69
|
+
|
|
70
|
+
## One turn of the cycle
|
|
71
|
+
|
|
72
|
+
Every step is callable **by anyone**. On an ETH-quoted vault each one refunds its
|
|
73
|
+
own gas at the real cost, priced at `block.basefee` — which the caller does not
|
|
74
|
+
choose — and capped. `publishRoot` is the single exception.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
harvest() pull the creator fees out of Pons, split them
|
|
78
|
+
│ platform (fixed at birth) · rewards (rewardsBps,
|
|
79
|
+
│ raisable only) · creator (the residue)
|
|
80
|
+
▼
|
|
81
|
+
buyBasket(minOuts[]) buy the WHOLE basket for every epoch since the last
|
|
82
|
+
│ purchase — one shared hop into the pivot, then one
|
|
83
|
+
│ leg per stock, each above its own price floor,
|
|
84
|
+
│ delivered straight to the Distributor
|
|
85
|
+
▼
|
|
86
|
+
publishRoot(...) KEEPER ONLY. Commit the cumulative root, immediately
|
|
87
|
+
│ in force. Preceded by five preflight checks.
|
|
88
|
+
▼
|
|
89
|
+
distribute(holder, ...) push a holder's shares to them. Or the holder calls
|
|
90
|
+
claim(...) and pays their own gas — or collect(...)
|
|
91
|
+
and settles every launch they hold at once.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
An epoch nobody buys for simply carries over: the money stays in the vault and
|
|
95
|
+
the next purchase covers every epoch that went by. Nothing is lost and nothing is
|
|
96
|
+
stuck.
|
|
97
|
+
|
|
98
|
+
### One purchase, the whole basket
|
|
99
|
+
|
|
100
|
+
Payd bought **one stock per epoch** and honoured the weights by rotation. The
|
|
101
|
+
value was fair; the composition was noise — at a 24-hour cadence a holder needed
|
|
102
|
+
ten days to see a whole basket go by. A purchase now covers a **window**: every
|
|
103
|
+
epoch the Distributor has not been funded for, up to the last one that has
|
|
104
|
+
finished. ([§S41](ARCHITECTURE.md#s41--one-purchase-takes-the-whole-basket-and-a-skipped-leg-brings-nothing-down),
|
|
105
|
+
which supersedes [§S16](ARCHITECTURE.md#s16--one-epoch-one-stock-weighted-rotation))
|
|
106
|
+
|
|
107
|
+
What the window shares, instead of paying per leg: the hop into the pivot
|
|
108
|
+
currency (139,625 gas), the TWAP read of that same pool (69,389 gas), the
|
|
109
|
+
ETH/USD feed, the base transaction, the refund, and one `fundWindow` instead of
|
|
110
|
+
one `fund` per epoch.
|
|
111
|
+
|
|
112
|
+
**A leg that cannot be bought is skipped, not fatal.** A paused stock, a dry
|
|
113
|
+
pool, a floor the market will not meet: that leg's pivot currency stays in
|
|
114
|
+
`pivotReserve`, the contract emits `LegSkipped`, and the next purchase spends it.
|
|
115
|
+
A stock Robinhood pauses costs a delay, never a loss — and never the other legs.
|
|
116
|
+
This is what the `try/catch` buys back, and why `_fund` truncates the arrays to
|
|
117
|
+
the legs that actually bought: `fundWindow` refuses a zero amount, so passing the
|
|
118
|
+
full-width arrays made one skipped leg revert the entire purchase.
|
|
119
|
+
|
|
120
|
+
Two legs never touch a pool at all:
|
|
121
|
+
|
|
122
|
+
- a **pivot line** (USDG here) is already in the basket's currency. There is no
|
|
123
|
+
pool of a token against itself, so there is no floor to compute and no price to
|
|
124
|
+
protect. It is the one line `Payd` lists at tier 0;
|
|
125
|
+
- a line that **is the vault's own quote** is held back *before* the hop rather
|
|
126
|
+
than bought back after it. The round trip costs two pool fees and two
|
|
127
|
+
slippages to end up where it started — 0.10 % measured on NVDA/USDG at tier
|
|
128
|
+
500.
|
|
129
|
+
|
|
130
|
+
### One currency per vault, and the pivot it routes through
|
|
131
|
+
|
|
132
|
+
Pons takes `pairToken` as an argument of `launchToken`, and its escrow keeps
|
|
133
|
+
**one ledger per currency**. Measured over seven days of `V2FeeEscrow` credits
|
|
134
|
+
(2026-09-08): **40.9 %** of Pons volume is quoted in ETH, **22.0 %** in USDG,
|
|
135
|
+
**37.2 %** in stock tokens. A vault that reads only the ETH ledger leaves three
|
|
136
|
+
fifths of the market unreachable, which is what v1 did.
|
|
137
|
+
|
|
138
|
+
So a vault declares **one `QUOTE` at birth and never changes it** — every number
|
|
139
|
+
it holds is denominated in that currency, and `bind` refuses any launch quoted
|
|
140
|
+
elsewhere. Everything then routes through one **`PIVOT`** currency, USDG on this
|
|
141
|
+
chain, because that is where the stocks' liquidity is (`recon.md` §4.1).
|
|
142
|
+
|
|
143
|
+
**The pivot is a crossroads, not a wall.** A currency can be deeply traded and
|
|
144
|
+
still invisible against the pivot: COIN and cbBTC have no pivot pool at all, and
|
|
145
|
+
$33,144 / $158,774 of WETH depth — between them 198 of the ~220 weekly credits
|
|
146
|
+
among the otherwise-unreachable pairs. Those are reached by `QUOTE → WETH →
|
|
147
|
+
PIVOT`, whose second hop is the very pool an ETH-quoted vault already uses. The
|
|
148
|
+
route is **declared at birth from a measurement, never probed when the money
|
|
149
|
+
moves**: exactly one of `QUOTE_FEE` / `QUOTE_WETH_FEE` is non-zero.
|
|
150
|
+
|
|
151
|
+
Neither the pivot nor the WETH→pivot tier is a constant any more. Both are
|
|
152
|
+
written at `init` from `Payd`'s wiring, so a redeployed registry can point
|
|
153
|
+
somewhere else without a line of contract changing.
|
|
154
|
+
([§S40](ARCHITECTURE.md#s40--one-currency-per-vault-and-the-pivot-as-a-crossroads))
|
|
155
|
+
|
|
156
|
+
**A non-ETH vault skims no delivery budget, and pays its caller in its own
|
|
157
|
+
currency.** The refund is computed in wei and the Distributor spends wei; a vault
|
|
158
|
+
holding NVDA has neither. So the cycle pays a **bounty** instead — 2 % of what
|
|
159
|
+
the call moved, capped at about $25 — which needs no price feed because it is
|
|
160
|
+
already denominated in the vault's currency. The delivery budget has no such
|
|
161
|
+
escape — it is wei, and the Distributor cannot spend NVDA on gas — so the keeper
|
|
162
|
+
fronts every push there and the bounty is what pays for it. The airdrop floor is
|
|
163
|
+
**40 % of `MIN_BUY_QUOTE`, about $10** — the same value an ether vault uses,
|
|
164
|
+
simply expressed in the currency the vault actually holds.
|
|
165
|
+
|
|
166
|
+
### The price floor
|
|
167
|
+
|
|
168
|
+
Never `minOut = 0`. Every purchase prices through a **30-minute Uniswap v3
|
|
169
|
+
TWAP**, tightened by a **Chainlink** feed where one exists and never blocked by
|
|
170
|
+
one that is stale — equity feeds go quiet at the weekend, and around a split the
|
|
171
|
+
feed and the token's own multiplier disagree for an hour. The caller may pass a
|
|
172
|
+
tighter `minOut` per leg; the contract takes `max(caller, on-chain floor)`, so a
|
|
173
|
+
hostile caller can only make their own transaction fail.
|
|
174
|
+
([§S3](ARCHITECTURE.md#s3--minout-an-on-chain-floor-tightenable-off-chain-p3-p4))
|
|
175
|
+
|
|
176
|
+
Upstream of the floor, `Payd` refuses to list a stock or a quote whose
|
|
177
|
+
declared pool **does not exist or carries nothing**. Existence alone would have
|
|
178
|
+
caught nothing here: all four NVDA/USDG tiers exist and `getPool` answers on each
|
|
179
|
+
— it is the liquidity that separates them, 1.4e19 at tier 500 and **zero** at
|
|
180
|
+
tier 10000. ([§S42](ARCHITECTURE.md#s42--a-listed-line-must-have-a-pool-that-carries-something))
|
|
181
|
+
|
|
182
|
+
## The snapshot
|
|
183
|
+
|
|
184
|
+
An epoch's balances are the **time-weighted average over the whole epoch**: not
|
|
185
|
+
a balance read at some instant, but `∫ balance dt` divided by the epoch's length.
|
|
186
|
+
Hold for the full period and you weigh what you hold; hold for a second and you
|
|
187
|
+
weigh a second.
|
|
188
|
+
|
|
189
|
+
That leaves nothing to snipe, and nothing to draw. The period is
|
|
190
|
+
`[GENESIS + e·L, GENESIS + (e+1)·L)` — two immutables and a subtraction — so the
|
|
191
|
+
publisher chooses no part of it, and no seed has to be committed on-chain to
|
|
192
|
+
prove they did not. The two transactions per epoch that used to do that, and the
|
|
193
|
+
delay between them, are gone. ([§S38](ARCHITECTURE.md#s38--the-time-weighted-average-and-the-machinery-it-deletes))
|
|
194
|
+
|
|
195
|
+
`L` is per vault: `Payd` accepts anything from **30 minutes to 24 hours**.
|
|
196
|
+
Thirty minutes is what a token with real volume wants; a day is what a quiet one
|
|
197
|
+
wants, so its keeper is not publishing roots into the void.
|
|
198
|
+
|
|
199
|
+
Excluded from the snapshot: the Uniswap pool, the Pons bonding curve, the vault
|
|
200
|
+
and its Distributor, address zero, the burn address `0xdead`, and a list the timelock
|
|
201
|
+
maintains. That list is
|
|
202
|
+
**on-chain and dated**: an exclusion applies from a given epoch onward, so two
|
|
203
|
+
people replaying the same epoch on either side of a change still agree.
|
|
204
|
+
([§S23](ARCHITECTURE.md#s23--an-exclusion-is-dated-not-live))
|
|
205
|
+
|
|
206
|
+
## Two roots, and why cumulative
|
|
207
|
+
|
|
208
|
+
A leaf is `(holder, stock, cumulative)` — the total owed **since inception**, not
|
|
209
|
+
for one epoch. The contract remembers what it has already paid and settles only
|
|
210
|
+
the difference. Cost therefore scales with the number of **stocks**, never with
|
|
211
|
+
the number of epochs elapsed: settling a holder after a week costs the same as
|
|
212
|
+
after an hour, and replaying an old proof pays nothing.
|
|
213
|
+
([§S18](ARCHITECTURE.md#s18--cumulative-roots-cost-follows-the-stocks-no-longer-time))
|
|
214
|
+
|
|
215
|
+
Each publication carries **two** roots:
|
|
216
|
+
|
|
217
|
+
- **`claimRoot`** — every eligible holder. This is what `claim` verifies against,
|
|
218
|
+
and it is open to anyone at any time.
|
|
219
|
+
- **`pushRoot`** — only the entries worth delivering: a holder whose whole outstanding
|
|
220
|
+
share is worth ~$10.
|
|
221
|
+
`distribute` verifies against this one, so the gas refund can only ever fund a
|
|
222
|
+
delivery that was worth making.
|
|
223
|
+
|
|
224
|
+
Both are standard OpenZeppelin sorted-pair trees, checked leaf by leaf against
|
|
225
|
+
the reference implementation in `test/MerkleCompat.t.sol` and
|
|
226
|
+
`front/src/merkle.test.ts`. They are **different trees**, and a proof built on
|
|
227
|
+
the wrong one passes every check in the browser and reverts on-chain — the trap
|
|
228
|
+
§S39 documents.
|
|
229
|
+
|
|
230
|
+
## Verifying without asking anyone
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pnpm --filter offchain dispute
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Recomputes the shares from the chain alone — no key, no input from the project, no
|
|
237
|
+
archive node — and reports whether the published root matches. Everything it
|
|
238
|
+
consumes is public: the exclusions are on-chain and dated, the artifact is
|
|
239
|
+
committed as a sha256 digest in `Root.digest`, and the period each average covers
|
|
240
|
+
falls out of `GENESIS` and `EPOCH_LENGTH`.
|
|
241
|
+
|
|
242
|
+
The keeper runs **five preflight checks** before publishing and refuses to
|
|
243
|
+
publish if any fails — conservation, monotonicity, population, provenance, and a
|
|
244
|
+
full recomputation on a second RPC. Publishing nothing delays rewards;
|
|
245
|
+
publishing wrongly misdirects them.
|
|
246
|
+
([§S32](ARCHITECTURE.md#s32--the-preflight-refusing-to-publish-rather-than-publishing-wrongly))
|
|
247
|
+
|
|
248
|
+
## Who can do what
|
|
249
|
+
|
|
250
|
+
| Actor | Can | Cannot |
|
|
251
|
+
| :--- | :--- | :--- |
|
|
252
|
+
| **Anyone** | harvest, buy the basket, pay the creator and the platform their share, distribute, claim, collect across launches, top up the rewards reserve, split the Treasury and run its four pockets | change any parameter |
|
|
253
|
+
| **Keeper** | publish a root | move funds, change anything else |
|
|
254
|
+
| **Creator** (of one launch) | raise their vault's `rewardsBps` — **upwards only**, floor 50 % | lower it, touch the basket, touch funds |
|
|
255
|
+
| **Timelock** (48 h, proposed by the Safe, executed by anyone) | per vault: reweight the basket, set the payout rate, set the gas share, manage exclusions, rotate the keeper, `migrate` the fee stream. Platform-wide: list and delist stocks and quotes, set `platformBps` for FUTURE vaults, set the Treasury split, admit a factory the generation key has approved (`setFactory`, `enableFactory`) and stop one building (`disableFactory`) | withdraw, redirect to itself, or freeze anything |
|
|
256
|
+
| **Safe** (2-of-3) | launch its own token, propose to the timelock | touch funds already in the contracts, or reach a creator's launch |
|
|
257
|
+
| **Generation key** (cold, immutable, holds nothing) | `approve` a candidate factory — and revoke it while the timelock has not executed. Open or shut `crossModeMigration`, the one thing that lets a migration cross payout modes | name anything by itself: `setFactory` and `enableFactory` are still the timelock's, still 48 h, and opening the cross-mode door migrates nothing on its own |
|
|
258
|
+
|
|
259
|
+
`platformBps` is stamped into each vault **at its birth** and is immutable there:
|
|
260
|
+
changing it on `Payd` reaches only vaults that do not exist yet, so a
|
|
261
|
+
creator knows at launch what the platform takes, for good.
|
|
262
|
+
|
|
263
|
+
`migrate` is the only door out of a vault, and it leads to exactly one place: a
|
|
264
|
+
vault of the **same token, same creator, same currency and the same payout
|
|
265
|
+
mode**, that pays holders at least as well and takes no more for the platform. **Not one stock leaves the
|
|
266
|
+
Distributor** — what was already credited stays claimable where it is; the vault's
|
|
267
|
+
own undelivered reserve follows the stream, into the successor's rewards pool and
|
|
268
|
+
pivot reserve, which are one-way pockets there too (`_moveReserve`). It exists
|
|
269
|
+
because only the *current* fee recipient can replace itself, so without it a bug
|
|
270
|
+
would strand a token's fee stream forever.
|
|
271
|
+
|
|
272
|
+
The destination is checked against **the registry, and nothing the destination
|
|
273
|
+
itself says**: `Payd.isVault`, a flag written by `_create` and by nothing else,
|
|
274
|
+
and `Payd.modeOf`, stamped there in the same breath. It used to be `recognised`,
|
|
275
|
+
which walked a chain of
|
|
276
|
+
`setSuccessor` pointers so a vault of another generation could be accepted — and
|
|
277
|
+
that was the largest residual hole in the system: `setSuccessor` took any address,
|
|
278
|
+
and five lines answering `isVault(x) = true` for every `x` opened the migration,
|
|
279
|
+
hence the future flow *and* the reserve, onto anything. No check could close it,
|
|
280
|
+
because everything you read from an unknown contract is written by that contract.
|
|
281
|
+
It existed only because the implementations lived inside the registry as
|
|
282
|
+
`immutable`, so a new vault version forced a new registry. `DistributionFactory` carries
|
|
283
|
+
them now, a new version is born in **this** registry, and the successor chain has
|
|
284
|
+
been deleted rather than guarded.
|
|
285
|
+
[§S24](ARCHITECTURE.md#s24--the-escape-valve-and-why-it-points-at-the-safe)
|
|
286
|
+
records the version that pointed at a maintainer-controlled Safe, and why it no
|
|
287
|
+
longer does.
|
|
288
|
+
|
|
289
|
+
**The mode is the bound that was added rather than removed.** `isVault` says the
|
|
290
|
+
destination was born here; it does not say it pays the same way. While one mode
|
|
291
|
+
exists the two questions have the same answer — the day a second factory builds
|
|
292
|
+
something else they do not, and one timelock operation would move a pro-rata
|
|
293
|
+
stream into another payout shape. What is compared is what the two vaults *pay*,
|
|
294
|
+
not which deployment made them, so a new version of the same mode still migrates:
|
|
295
|
+
that is the entire point of the function. Crossing modes takes a second key that
|
|
296
|
+
is not the timelock's — the generation key opens `crossModeMigration`, shut from
|
|
297
|
+
birth — and even open, it only lets the timelock schedule a migration that must
|
|
298
|
+
still clear 48 h and every other condition above.
|
|
299
|
+
[§S46](ARCHITECTURE.md#s46--one-payout-mode-is-one-factory-and-the-stamp-that-keeps-them-apart)
|
|
300
|
+
has the reasoning and what it deliberately does not do.
|
|
301
|
+
|
|
302
|
+
## The code
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
contracts/ FeeVault · Distributor · Payd · DistributionFactory · Treasury
|
|
306
|
+
Collector · Timelock · Bootstrap
|
|
307
|
+
interfaces/ every external ABI, each one read on-chain and dated
|
|
308
|
+
libraries/ TwapFloor + the Uniswap maths it needs, ported to 0.8
|
|
309
|
+
test/ fork tests against live state — no mock on Pons or Uniswap
|
|
310
|
+
Invariants.t.sol what must hold after ANY sequence of calls
|
|
311
|
+
script/ Deploy · DeployPayd · Allowlist · Quotelist · the Measure* recons
|
|
312
|
+
offchain/src/
|
|
313
|
+
snapshot.ts replay Transfer logs, weight each balance by the time it was held
|
|
314
|
+
eligibility.ts who is in the tree — a pure function, replayable
|
|
315
|
+
epoch.ts build the cumulative artifact and both roots
|
|
316
|
+
preflight.ts the five checks that block a publication
|
|
317
|
+
keeper.ts the loop: every step idempotent, restartable with no memory
|
|
318
|
+
dispute.ts recompute a published root from the chain alone
|
|
319
|
+
front/ the claim page: reads the chain, rebuilds the tree, proves
|
|
320
|
+
site/ the shop window
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
A test that only passes thanks to a mock on Pons or Uniswap is rejected. The
|
|
324
|
+
suite runs against the real chain, with real pools and real stock tokens taken
|
|
325
|
+
from real holders.
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
forge test --fork-url $RPC_URL_FALLBACK --compute-units-per-second 60 -j 1
|
|
329
|
+
pnpm --filter offchain test && pnpm --filter front test
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## What can go wrong
|
|
333
|
+
|
|
334
|
+
Stated in the README for holders, repeated here for readers of the code:
|
|
335
|
+
Robinhood can pause or block a stock token and this protocol is exposed like
|
|
336
|
+
everyone else — a paused stock now costs a skipped leg rather than a failed
|
|
337
|
+
purchase, but it still costs; Pons can redirect the fee stream with three days'
|
|
338
|
+
notice and no veto from the vault; the publishing service stopping means fees pile
|
|
339
|
+
up undistributed until it comes back. And **no external audit has been done** —
|
|
340
|
+
[`SECURITY.md`](../SECURITY.md) says what is in scope and how to report.
|
|
341
|
+
|
|
342
|
+
## Where to go next
|
|
343
|
+
|
|
344
|
+
| | |
|
|
345
|
+
| :--- | :--- |
|
|
346
|
+
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | 42 decisions with their measurements — including the ones we got wrong |
|
|
347
|
+
| [`recon.md`](recon.md) | Every external address, how it was verified, on what date |
|
|
348
|
+
| [`CONVENTIONS.md`](CONVENTIONS.md) | The rules the code is held to, and the measurement behind each |
|
|
349
|
+
| [`recon-launchpad.md`](recon-launchpad.md) | The same, for what the registry had to learn |
|
|
350
|
+
| [`allowlist.md`](allowlist.md) | The stocks and quotes, with the depth measured behind each |
|
|
351
|
+
| [`../SECURITY.md`](../SECURITY.md) | Scope, the known trade-offs, how to report a finding |
|
package/docs/SDK.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# SDK.md — integrating a Payd vault by hand
|
|
2
|
+
|
|
3
|
+
`sdk/README.md` covers the two-line drop-in. This is the other document: every
|
|
4
|
+
call the SDK makes, in order, so you can rebuild exactly the part you want in
|
|
5
|
+
whatever stack you already ship — viem, ethers, wagmi, web3.py, a Go backend, a
|
|
6
|
+
Dune query, a Telegram bot.
|
|
7
|
+
|
|
8
|
+
Nothing here needs a key, a server, or our permission. It is all public state on
|
|
9
|
+
Robinhood Chain.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
chain id 4663
|
|
13
|
+
rpc https://rpc.mainnet.chain.robinhood.com
|
|
14
|
+
explorer https://robinhoodchain.blockscout.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. The one address you need
|
|
20
|
+
|
|
21
|
+
A launch has **one vault**. Everything else hangs off it:
|
|
22
|
+
|
|
23
|
+
```solidity
|
|
24
|
+
vault.DISTRIBUTOR() → address // where shares are settled
|
|
25
|
+
vault.token() → address // the launched ERC-20
|
|
26
|
+
vault.CREATOR() → address // you
|
|
27
|
+
vault.QUOTE() → address // the currency fees arrive in; 0x0 = native ETH
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
There is **one Distributor per vault**, cloned at launch. Do not hardcode it,
|
|
31
|
+
and do not reuse another launch's — read it from your vault every time. Same for
|
|
32
|
+
the token: a vault knows its token, so a page that takes both as configuration
|
|
33
|
+
has two chances to be wrong instead of one.
|
|
34
|
+
|
|
35
|
+
`Payd.vaultsOf(creator) → address[]` lists every vault you own, and
|
|
36
|
+
`Payd.vaults()` lists all of them.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. What your token pays — `economics()`
|
|
41
|
+
|
|
42
|
+
```solidity
|
|
43
|
+
vault.economics() → (
|
|
44
|
+
uint256 taxBps, // creator tax on every trade
|
|
45
|
+
uint256 curveFeeBps, // the Pons curve fee
|
|
46
|
+
uint256 ponsShareBps, // the slice of that fee Pons keeps
|
|
47
|
+
uint256 grossOfVolumeBps, // what reaches the vault, as a share of VOLUME
|
|
48
|
+
uint256 rewardsOfVolumeBps, // …of which, to holders as stock
|
|
49
|
+
uint256 creatorOfVolumeBps, // …to you
|
|
50
|
+
uint256 platformOfVolumeBps // …to Payd, fixed at creation, never raisable
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**These are shares of traded volume, not of the vault.** That is deliberate: it
|
|
55
|
+
is what a trader actually pays, and the only one of the two figures you can
|
|
56
|
+
compare from one launch to another. `rewardsOfVolumeBps = 250` means *2.5 % of
|
|
57
|
+
every trade comes back to holders as stock* — that is your headline number, and
|
|
58
|
+
it is different for every launch, so it cannot be written into a template.
|
|
59
|
+
|
|
60
|
+
The call reverts to zeros if a Pons getter moves under it. Show nothing rather
|
|
61
|
+
than a stale percentage; that is what the SDK does.
|
|
62
|
+
|
|
63
|
+
## 3. Is it still working — `hookStatus()`
|
|
64
|
+
|
|
65
|
+
```solidity
|
|
66
|
+
vault.hookStatus() → (uint8 status, address current, uint64 effectiveAt)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| status | meaning |
|
|
70
|
+
|---|---|
|
|
71
|
+
| 0 | not launched — the vault exists, no token is paying into it yet |
|
|
72
|
+
| 1 | **collecting** — the healthy state |
|
|
73
|
+
| 2 | redirect scheduled — fees will move at `effectiveAt` |
|
|
74
|
+
| 3 | fees lost — the creator wallet on Pons is no longer this vault |
|
|
75
|
+
|
|
76
|
+
Anything other than 1 deserves a visible badge on your page. A card that shows a
|
|
77
|
+
cheerful percentage while status is 3 is lying to your holders.
|
|
78
|
+
|
|
79
|
+
## 4. The basket — `getAllocations()`
|
|
80
|
+
|
|
81
|
+
```solidity
|
|
82
|
+
vault.getAllocations() → (address stock, uint24 poolFee, uint16 bps, address feed)[]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Two to eight Robinhood stock tokens (`MIN_BASKET` / `MAX_BASKET`), weights in
|
|
86
|
+
bps summing to 10 000 and no line under `MIN_ALLOC_BPS` (1 000). Read the array's
|
|
87
|
+
length rather than assuming it. One purchase buys
|
|
88
|
+
the *whole* basket, each line by its weight. `poolFee` and `feed` are plumbing —
|
|
89
|
+
the Uniswap tier and the oracle used for the slippage floor. For a UI you want
|
|
90
|
+
`stock` (call `symbol()` on it) and `bps`.
|
|
91
|
+
|
|
92
|
+
`allocationOf(uint256)` and `ROTATION_STRIDE()` **no longer exist** (removed
|
|
93
|
+
2026-09-09). They survived from a weighted-rotation design — one stock per epoch
|
|
94
|
+
— replaced by buying the whole basket in one transaction. Reading them reverts.
|
|
95
|
+
|
|
96
|
+
They were removed for a measured reason, not for tidiness: `FeeVault` was 26 152
|
|
97
|
+
bytes of runtime, **1 576 above the EIP-170 cap**, and those two dead members
|
|
98
|
+
were 1 322 of the way back under.
|
|
99
|
+
|
|
100
|
+
## 5. The clock — the Distributor
|
|
101
|
+
|
|
102
|
+
```solidity
|
|
103
|
+
distributor.currentEpoch() → uint256
|
|
104
|
+
distributor.epochEnd(epoch) → uint256 // unix timestamp
|
|
105
|
+
distributor.EPOCH_LENGTH() → uint256 // 30 min in production
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`epochEnd(currentEpoch()) - now` is your countdown to the next buy. Nothing
|
|
109
|
+
about the epoch needs a subscription or a websocket; two reads on page load and
|
|
110
|
+
a local timer are enough, since `epochEnd` only changes when the epoch rolls.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 6. Paying a holder — the settlement path
|
|
115
|
+
|
|
116
|
+
This is the part with a sharp edge. Read it before you write it.
|
|
117
|
+
|
|
118
|
+
### 6.1 The root
|
|
119
|
+
|
|
120
|
+
Every epoch the keeper publishes one root:
|
|
121
|
+
|
|
122
|
+
```solidity
|
|
123
|
+
distributor.activeRoot() → uint256 // 0 = none published yet
|
|
124
|
+
distributor.roots(id) → (
|
|
125
|
+
address publisher, uint40 publishedAt,
|
|
126
|
+
bytes32 claimRoot, bytes32 pushRoot,
|
|
127
|
+
uint48 upToEpoch, bytes32 digest
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> Decode that tuple **field for field**. Dropping one raises no error in most
|
|
132
|
+
> libraries — the decoder reads a word too early and everything after it shifts
|
|
133
|
+
> by one slot, so `digest` silently returns `upToEpoch` and you never find the
|
|
134
|
+
> data. This has bitten this codebase; it is why the ABI in `front/src/chain.ts`
|
|
135
|
+
> carries a warning comment.
|
|
136
|
+
|
|
137
|
+
### 6.2 The artifact, and why you must not trust the gateway
|
|
138
|
+
|
|
139
|
+
`digest` is `sha256(the canonical epoch JSON)`. The JSON itself lives on IPFS.
|
|
140
|
+
Two ways to address it, in this order:
|
|
141
|
+
|
|
142
|
+
1. the `cid` string in the `RootPublished` log for that `rootId` — the only one
|
|
143
|
+
that works once the artifact exceeds one IPFS block, around 160 holders;
|
|
144
|
+
2. the CIDv1 rebuilt from the digest (`raw`, `sha2-256`) — correct while it fits
|
|
145
|
+
in one block, and your only option if the chain no longer serves old logs.
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
event RootPublished(uint256 indexed rootId, address indexed publisher,
|
|
149
|
+
bytes32 claimRoot, bytes32 pushRoot,
|
|
150
|
+
uint256 upToEpoch, bytes32 digest, string cid)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Then hash what you fetched and compare it to `digest` before you use a single
|
|
154
|
+
byte of it.** Gateways are a convenience, not an authority. With the check, a
|
|
155
|
+
hostile or broken gateway can only make your page fail to load. Without it, it
|
|
156
|
+
can invent amounts and addresses in your UI. Try gateways in order and take the
|
|
157
|
+
first whose content matches; `ipfs.io` and `dweb.link` return a 403 Cloudflare
|
|
158
|
+
page to any browser fetch carrying an `Origin` header, so they belong at the end
|
|
159
|
+
of the list, never alone.
|
|
160
|
+
|
|
161
|
+
The JSON:
|
|
162
|
+
|
|
163
|
+
```jsonc
|
|
164
|
+
{
|
|
165
|
+
"upToEpoch": 412,
|
|
166
|
+
"excluded": ["0x…"], // pool, curve, vault, distributor, CEXs
|
|
167
|
+
"entries": [
|
|
168
|
+
{ "holder": "0x…", "stock": "0x…",
|
|
169
|
+
"cumulative": "1234567890", // raw units, since genesis
|
|
170
|
+
"push": true } // large enough to be airdropped
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Shares are **cumulative since genesis**, not per-epoch. One claim settles the
|
|
176
|
+
whole history, one transfer per stock, no matter how long the holder waited.
|
|
177
|
+
|
|
178
|
+
### 6.3 Two trees, and the mistake that costs a transaction
|
|
179
|
+
|
|
180
|
+
```solidity
|
|
181
|
+
distributor.claim(address[] stocks, uint256[] cumulative, bytes32[][] proofs)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`claim()` — signed by the holder for themselves — verifies against
|
|
185
|
+
**`claimRoot`**, built over **every** entry.
|
|
186
|
+
`distribute()` — callable by anyone for a third party, and it refunds its own
|
|
187
|
+
gas — verifies against **`pushRoot`**, built over the entries with
|
|
188
|
+
`push: true` only.
|
|
189
|
+
|
|
190
|
+
A proof built on the wrong tree passes every check you can make off-chain and
|
|
191
|
+
reverts on-chain with `InvalidProof`. Build the claim tree from
|
|
192
|
+
`entries`, the push tree from `entries.filter(e => e.push)`, and never mix them.
|
|
193
|
+
|
|
194
|
+
### 6.4 The tree shape
|
|
195
|
+
|
|
196
|
+
The leaf is a **double keccak** of `abi.encode(address holder, address stock,
|
|
197
|
+
uint256 cumulative)`. Leaves are sorted by hash, laid out backwards in the second
|
|
198
|
+
half of a complete binary tree of `2n-1` nodes, and paired with an ordered hash
|
|
199
|
+
(`a < b ? H(a,b) : H(b,a)`). That is OpenZeppelin's `StandardMerkleTree`, and it
|
|
200
|
+
is what fixes the root.
|
|
201
|
+
|
|
202
|
+
If you are in JS, use `@openzeppelin/merkle-tree` and stop thinking about it.
|
|
203
|
+
If you are anywhere else, port it and test it: **naively pairing sorted leaves
|
|
204
|
+
two by two gives the same root at 2, 3, 4, 6 and 8 leaves and diverges at 5, 7,
|
|
205
|
+
9** — so every quick trial passes and every real epoch reverts.
|
|
206
|
+
`front/src/merkle.ts` is a dependency-free implementation, checked leaf by leaf
|
|
207
|
+
against the real library from 1 to 200 entries in `front/src/merkle.test.ts`.
|
|
208
|
+
|
|
209
|
+
### 6.5 Before you send
|
|
210
|
+
|
|
211
|
+
```solidity
|
|
212
|
+
distributor.owedTo(holder, stock, cumulative) → uint256 // still to be paid
|
|
213
|
+
distributor.claimedSoFar(holder, stock) → uint256 // already received
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- **Drop any stock where `owedTo` is 0.** It passes the Merkle check and
|
|
217
|
+
delivers nothing, and the caller pays for its branch anyway.
|
|
218
|
+
- **Re-read `activeRoot` and rebuild the proofs immediately before sending.** A
|
|
219
|
+
tab left open across a publication holds proofs the contract will reject, and
|
|
220
|
+
`_settle` reverts the *whole* batch on the first `InvalidProof` — the wallet
|
|
221
|
+
shows a bare "execution reverted" that explains nothing to your user. One
|
|
222
|
+
extra read costs less than one failed transaction.
|
|
223
|
+
- `owed` is in the stock token's own units. Read its `decimals()`. It is not ETH.
|
|
224
|
+
|
|
225
|
+
### 6.6 You usually do not have to do any of this
|
|
226
|
+
|
|
227
|
+
Shares arrive on their own: the automatic airdrop runs at most every 24 h, as
|
|
228
|
+
soon as a holder's whole outstanding share is worth about $10. A claim button is a *shortcut* for
|
|
229
|
+
someone who does not want to wait — never the only door. If building section 6
|
|
230
|
+
is more than you want to carry, ship sections 1–5 and link to
|
|
231
|
+
[paydprotocol.eth/app](https://paydprotocol.eth.limo/app/) for the collection.
|
|
232
|
+
|
|
233
|
+
`Collector.collect(account, distributors, stocks, cumulative, proofs)` settles
|
|
234
|
+
several launches in one transaction for holders of more than one Payd token. It
|
|
235
|
+
holds nothing and has no owner; it is a shortcut too, and a broken or replaced
|
|
236
|
+
one never stands between anyone and their share.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 7. Who is not in the tree
|
|
241
|
+
|
|
242
|
+
Excluded from the snapshot, and therefore paid nothing: the Uniswap pool, the
|
|
243
|
+
Pons bonding curve, the vault, the distributor, address 0, and a timelocked
|
|
244
|
+
`excluded[]` list (CEXs, contracts). Read it with
|
|
245
|
+
`distributor.excludedList()`, or `isExcludedAt(account, epoch)` for a past
|
|
246
|
+
epoch.
|
|
247
|
+
|
|
248
|
+
Holders below the value threshold are not in the tree either, and their weight
|
|
249
|
+
is redistributed to the others. The threshold is expressed as the **value of the
|
|
250
|
+
share**, not a percentage of supply — a fixed percentage is too permissive at
|
|
251
|
+
launch and too restrictive later.
|
|
252
|
+
|
|
253
|
+
An epoch's balance is the **time-weighted average over the whole epoch**,
|
|
254
|
+
`∫ balance dt / L`, not a sample. Someone who holds for an instant is paid for
|
|
255
|
+
an instant. If a holder asks why their number looks lower than their balance,
|
|
256
|
+
that is the answer.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 8. Things that will look like bugs and are not
|
|
261
|
+
|
|
262
|
+
| What you see | What it is |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `shares()` returns nothing for a real holder | they are not in the *current* root — bought after the last publication, or below the threshold. Next epoch fixes it. |
|
|
265
|
+
| `shares()` throws `epoch data not found on any gateway` | the root exists but no gateway served its artifact (since 2026-10-07; it used to return `[]`, which told a holder they had nothing). Retry; it is not their share. |
|
|
266
|
+
| `activeRoot() == 0` | no root published yet. A brand-new launch. Show "preparing the first payout". |
|
|
267
|
+
| `economics()` returns zeros | a Pons getter moved. Hide the block; do not show 0 %. |
|
|
268
|
+
| the artifact fetch fails on every gateway | the content is fine, the gateways are not. Retry, add gateways. Never fall back to unverified content. |
|
|
269
|
+
| a claim reverts with `InvalidProof` | a new root was published between building and sending, or you used the push tree. See 6.3 and 6.5. |
|
|
270
|
+
| a vault whose `QUOTE` is not ETH refunds no gas | by design — it pays a **bounty** instead: 2 % of what the call moved, in its own currency, capped at `MIN_BUY_QUOTE`. `_refundAmount` is in wei and that vault holds none. |
|
|
271
|
+
| `allowQuotes` reverts `QuoteNotSweepable` | the platform's till has no route to convert that currency back into ether. `Treasury.allowSweeps` has to land first — both are timelock votes, so it is an ordering, not a blocker. |
|
|
272
|
+
| a vault whose `QUOTE` is not ETH uses a floor in its own currency | its delivery floor is 40 % of `MIN_BUY_QUOTE` (~$10) — the same value an ether vault uses, but `quoteSpent` is denominated in the vault's QUOTE there, so a wei constant never matched. Same cadence, different unit. |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 9. Running the cycle yourself — keepers, bots and agents
|
|
277
|
+
|
|
278
|
+
Every step that moves a launch's money forward is **open to anyone** and pays
|
|
279
|
+
whoever calls it. A keeper runs them today; nothing stops a second one, and more
|
|
280
|
+
callers only make the cycle livelier — none of these calls gives its caller any
|
|
281
|
+
say over where the money goes.
|
|
282
|
+
|
|
283
|
+
| Call | On | What it does | What the caller gets |
|
|
284
|
+
|---|---|---|---|
|
|
285
|
+
| `harvest()` | vault | pulls creator fees out of the Pons escrow and splits them | ETH vault: its gas back, out of the rewards share. Other quotes: a bounty, `keeperBountyBps` of what it moved, in the vault's own currency |
|
|
286
|
+
| `buyBasket(uint256[] minOuts)` | vault | buys the whole basket for every closed epoch not yet bought | the same refund or bounty, capped |
|
|
287
|
+
| `distribute(account, stocks, cumulative, proofs)` | distributor | pushes one holder's share to **that holder** | its gas back, bounded by the value delivered — on an ETH vault only; on any other quote the delivery budget is zero and the call pays nothing |
|
|
288
|
+
| `payCreator()` / `payPlatform()` | vault | sends an accrued share to its fixed address | nothing — but it costs nobody anything either |
|
|
289
|
+
|
|
290
|
+
Four things that save a wasted transaction:
|
|
291
|
+
|
|
292
|
+
- **`minOuts` can only tighten.** The vault applies `max(minOuts[i], its own
|
|
293
|
+
oracle floor)` per leg, so an array of zeros of the basket's length is a valid
|
|
294
|
+
call and the slippage bound still holds. Never derive a floor from a model or
|
|
295
|
+
a quote API and pass it as a loosening — it cannot loosen, it can only make
|
|
296
|
+
your own call fail.
|
|
297
|
+
- **Simulate first.** `NothingToDo` (nothing to harvest, no closed epoch to buy)
|
|
298
|
+
and `NothingDelivered` (the share already left) are the normal answers when
|
|
299
|
+
another caller got there first. A reverted call is paid nothing.
|
|
300
|
+
- **`distribute` takes the push tree.** Entries with `push: true` only, proofs
|
|
301
|
+
built from `entries.filter(e => e.push)` — section 6.3.
|
|
302
|
+
- **A wallet that refuses ETH within 30 000 gas is not lost money.** The refund
|
|
303
|
+
is set aside instead of sent, and the caller collects it with the contract's
|
|
304
|
+
own `withdraw()`. Smart-contract and 7702-delegated agent wallets hit this.
|
|
305
|
+
|
|
306
|
+
What stays closed: `publishRoot`. A root names who is owed what, so it takes the
|
|
307
|
+
distributor's keeper or an address `Payd.isKeeper` names — both behind the
|
|
308
|
+
timelock.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 10. Source of truth
|
|
313
|
+
|
|
314
|
+
Everything above is read from the contracts in `contracts/`. When this document
|
|
315
|
+
and the code disagree, the code wins — and the disagreement is a bug worth
|
|
316
|
+
reporting.
|
|
317
|
+
|
|
318
|
+
- `contracts/FeeVault.sol` — fees in, basket out
|
|
319
|
+
- `contracts/Distributor.sol` — roots, claims, exclusions
|
|
320
|
+
- `contracts/Payd.sol` — the vault index
|
|
321
|
+
- `sdk/src/payd.ts` — a working implementation of this whole document, ~300 lines
|
|
322
|
+
- `front/src/merkle.ts` + `merkle.test.ts` — the tree, and its proof that it is right
|
|
323
|
+
- `docs/ARCHITECTURE.md` — why each of these choices, at length
|