alberta-buck-core 0.1.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.
@@ -0,0 +1,517 @@
1
+ // buildEquilibriumWorld: the minimal BUCK equilibrium world on any
2
+ // session -- deploy.py's basket world with 1..N tokens, on the REAL
3
+ // routing periphery. Promoted from the eqworld.tevm.test.js spike.
4
+ //
5
+ // * the proven identity + Direct stack (buildBuckWorld),
6
+ // * BuckBasketProRata + its Uniswap venue facet (union ABI: facet
7
+ // views like basketValueInBuck serve via the shell's fallback),
8
+ // * per token: a TOKEN/USDC reference pool (SimLP-seeded, whale
9
+ // territory) and the TOKEN/BUCK basket pool (addBasketToken),
10
+ // * the floating BUCK/USDC pool, LP'd by SimLP as the public
11
+ // BUCK-backed LP (zero-premium credit -> Buck.mint -> LP),
12
+ // * the Universal Router, identity-bound public+carrying -- ALL
13
+ // agent/user swaps route through it (the SimLP doctrine).
14
+ //
15
+ // The world carries the AGENT-FACING OP HELPERS the platform doctrine
16
+ // promises (agents never touch custody, identity binding, or router
17
+ // encoding): pledge / buyBuck / sellBuck / route / buyTokenBest /
18
+ // basketDeposit / basketRedeem / refUsd / receiptValueUsd / fiatIn,
19
+ // plus reads (spotUB / spotUsd / spotBuck / bvib / K) and a per-day
20
+ // series recorder for the charts.
21
+ //
22
+ // Custody pattern: EOA holders send their own txs (private->public
23
+ // transfers to the bound router are handshake-exempt). CREDIT-drawing
24
+ // holders get a PROXY (a SimLP instance bound public+NON-carrying --
25
+ // carrying accounts cannot draw negative), created lazily by pledge();
26
+ // helpers dispatch on holderAddress(). Fiat legs (income, endowments)
27
+ // enter as MockERC20 mints via fiatIn -- the sim's fiat bridge.
28
+
29
+ import { encodeFunctionData, keccak256, toBytes } from "viem";
30
+
31
+ import { advanceTime, buildBuckWorld, DAY } from "../buckworld.js";
32
+ import { deployUniversalRouter, encodePath, urExecArgs } from "../router.js";
33
+ import { Q96, fullRangeTicks, sqrtPriceX96, spotFromSqrtPriceX96 } from "../v3.js";
34
+ import { seededWalk } from "../prices.js";
35
+
36
+ export const FEES = { usdc: 3000, buck: 3000, ub: 500 };
37
+ const SPACING = { 3000: 60, 500: 10 };
38
+ const E18 = 10n ** 18n;
39
+ const DEPOSITED_TOPIC = keccak256(toBytes(
40
+ "Deposited(address,uint256,address,uint256,uint256,uint128)"));
41
+
42
+ // The public bind identity (sim/identity.py BIND_PK/BIND_E: the G1
43
+ // generator); BN254.sol ABI struct components are UPPERCASE.
44
+ const G = { X: 1n, Y: 2n };
45
+ const BIND_E = { R: G, C: G };
46
+
47
+ const DEFAULT_TOKENS = [
48
+ { sym: "CNST", name: "Construction", dec: 18, p0: 2_500_000n },
49
+ ];
50
+
51
+ /** Integer square root (Newton), for the unwind's impact cap. */
52
+ function isqrt(n) {
53
+ if (n < 2n) return n;
54
+ let x = n;
55
+ let y = (x + 1n) / 2n;
56
+ while (y < x) { x = y; y = (x + n / x) / 2n; }
57
+ return x;
58
+ }
59
+
60
+ /**
61
+ * @param session a Session (deployer = session.account)
62
+ * @param artifacts (name) => {abi, bytecode}
63
+ * @param opts.identity the buck-identity kernel API (REQUIRED)
64
+ * @param opts.urArtifact the parsed UniversalRouter.json (REQUIRED)
65
+ * @param opts.tokens [{sym, name, dec, p0}] basket constituents
66
+ * @param opts.targetBuck common pool depth, 6-dec (default $10M)
67
+ * @param opts.feedSeed / opts.stepBp / opts.feedDays the reference walks
68
+ * @param opts.rng scalar drawer for the issuer keypair
69
+ */
70
+ export async function buildEquilibriumWorld(session, artifacts, opts = {}) {
71
+ if (!opts.urArtifact) {
72
+ throw new Error("buildEquilibriumWorld needs opts.urArtifact " +
73
+ "(alberta_buck/sim/artifacts/UniversalRouter.json)");
74
+ }
75
+ const gas = 15_000_000n;
76
+ const toks = opts.tokens ?? DEFAULT_TOKENS;
77
+ const targetBuck = opts.targetBuck ?? 10n ** 13n;
78
+
79
+ const world = await buildBuckWorld(session, artifacts,
80
+ { identity: opts.identity, rng: opts.rng });
81
+ const { reg, credit, kctrl, buck } = world;
82
+ const me = session.account.address;
83
+ const bind = (addr, carrying, tag) =>
84
+ session.send(reg, "bindContract", [addr, G, BIND_E, true, carrying], { tag });
85
+
86
+ // --- factory, basket + venue, wiring (deploy.py order) --------------
87
+ const v3f = await session.deploy(artifacts("UniswapV3Factory"), [],
88
+ { name: "UniswapV3Factory", gas });
89
+ const basketArt = artifacts("BuckBasketProRata");
90
+ const basketShell = await session.deploy(basketArt,
91
+ [buck.address, kctrl.address, v3f.address, me, FEES.buck, 600, 64, 500, 1000],
92
+ { name: "BuckBasketProRata", gas });
93
+ const venue = await session.deploy(artifacts("BuckBasketUniswapV3"), [],
94
+ { name: "BuckBasketUniswapV3", gas });
95
+ await session.send(basketShell, "setVenue", [venue.address], { tag: "eq:setVenue" });
96
+ await session.send(buck, "setBasket", [basketShell.address], { tag: "eq:buck.setBasket" });
97
+ await session.send(kctrl, "setBasket", [basketShell.address], { tag: "eq:kctrl.setBasket" });
98
+ await bind(basketShell.address, true, "eq:bind:basket");
99
+ const facetAbi = artifacts("BuckBasketUniswapV3").abi;
100
+ const seen = new Set(basketArt.abi.map((e) => `${e.type}:${e.name}`));
101
+ const basket = session.contractAt(
102
+ basketArt.abi.concat(facetAbi.filter((e) => !seen.has(`${e.type}:${e.name}`))),
103
+ basketShell.address);
104
+
105
+ // --- USDC, tokens, SimLP ---------------------------------------------
106
+ const erc20Art = artifacts("MockERC20");
107
+ const usdc = await session.deploy(erc20Art, ["USD Coin", "USDC", 6],
108
+ { name: "USDC", gas });
109
+ const simlp = await session.deploy(artifacts("SimLP"), [], { name: "SimLP", gas });
110
+ await bind(simlp.address, false, "eq:bind:simlp"); // public, NON-carrying
111
+ await session.send(usdc, "mint", [simlp.address, 10n ** 30n],
112
+ { tag: "eq:stock-usdc" });
113
+
114
+ const poolAbi = artifacts("UniswapV3Pool").abi;
115
+ const tokens = [];
116
+ for (const t of toks) {
117
+ const erc20 = await session.deploy(erc20Art, [t.name, t.sym, t.dec],
118
+ { name: t.sym, gas });
119
+ await session.send(erc20, "mint", [simlp.address, 10n ** 30n],
120
+ { tag: `eq:stock-${t.sym}` });
121
+
122
+ // TOKEN/USDC reference pool, whale territory.
123
+ await session.send(v3f, "createPool", [erc20.address, usdc.address, FEES.usdc],
124
+ { tag: `eq:createPool:${t.sym}-usdc` });
125
+ const pu = await session.call(v3f, "getPool",
126
+ [erc20.address, usdc.address, FEES.usdc]);
127
+ const poolUsdc = session.contractAt(poolAbi, pu);
128
+ const unit = 10n ** BigInt(t.dec);
129
+ const sp = sqrtPriceX96(erc20.address, unit, usdc.address, t.p0);
130
+ await session.send(poolUsdc, "initialize", [sp], { tag: `eq:init:${t.sym}-usdc` });
131
+ const u0 = await session.call(poolUsdc, "token0");
132
+ const u1 = await session.call(poolUsdc, "token1");
133
+ const Lu = usdc.address.toLowerCase() === u0.toLowerCase()
134
+ ? (targetBuck * sp) / Q96 : (targetBuck * Q96) / sp;
135
+ const [lo, hi] = fullRangeTicks(SPACING[FEES.usdc]);
136
+ await session.send(simlp, "mint", [pu, lo, hi, Lu, u0, u1],
137
+ { tag: `eq:seed:${t.sym}-usdc` });
138
+
139
+ // TOKEN/BUCK basket pool (created + initialized by the basket).
140
+ await session.send(basket, "addBasketToken",
141
+ [erc20.address, t.dec, t.p0, 0, FEES.buck], { tag: `eq:addBasketToken:${t.sym}` });
142
+ const pb = await session.call(v3f, "getPool",
143
+ [erc20.address, buck.address, FEES.buck]);
144
+ await bind(pb, true, `eq:bind:pool-${t.sym}-buck`);
145
+
146
+ // Bootstrap the basket pool to real depth (the Python sim's pre-tick
147
+ // DM bootstrap phase): a targetBuck-sized deposit from the deployer,
148
+ // so agent-scale trades and the arb's daily corrections move the
149
+ // pool by basis points, not percent -- the redeem path's spot/TWAP
150
+ // guard (defaultMaxDeviationBp) assumes exactly this.
151
+ const seedTok = (targetBuck * unit) / t.p0;
152
+ await session.send(erc20, "mint", [me, seedTok],
153
+ { tag: `eq:bootstrap-stock:${t.sym}` });
154
+ await session.send(erc20, "approve", [basket.address, seedTok],
155
+ { tag: `eq:bootstrap-approve:${t.sym}` });
156
+ await session.send(basket, "depositToken", [erc20.address, seedTok, 0n],
157
+ { tag: `eq:bootstrap-deposit:${t.sym}`, gas: 3_000_000n });
158
+ tokens.push({ ...t, erc20, poolUsdc, poolBuck: session.contractAt(poolAbi, pb) });
159
+ }
160
+
161
+ // --- floating BUCK/USDC pool: SimLP is the BUCK-backed LP ------------
162
+ const k0 = await session.call(kctrl, "buckK");
163
+ const mintAmt = ((targetBuck * E18) / k0) * 12n / 10n;
164
+ const face2 = (mintAmt * 12n) / 10n;
165
+ const FACE = 2n * targetBuck > face2 ? 2n * targetBuck : face2;
166
+ const now = (await session.client.getBlock()).timestamp;
167
+ await session.send(credit, "createCredit",
168
+ [simlp.address, 0, FACE, 0n, 0, 0, now, 0], { tag: "eq:credit:simlp" });
169
+ await session.send(simlp, "exec",
170
+ [buck.address, encodeFunctionData(
171
+ { abi: buck.abi, functionName: "mint", args: [mintAmt] })],
172
+ { tag: "eq:simlp-mint-buck", gas: 3_000_000n });
173
+ await session.send(v3f, "createPool", [buck.address, usdc.address, FEES.ub],
174
+ { tag: "eq:createPool:buck-usdc" });
175
+ const pub = await session.call(v3f, "getPool", [buck.address, usdc.address, FEES.ub]);
176
+ const poolUB = session.contractAt(poolAbi, pub);
177
+ const spU = sqrtPriceX96(buck.address, 1_000_000n, usdc.address, 1_000_000n);
178
+ await session.send(poolUB, "initialize", [spU], { tag: "eq:init:buck-usdc" });
179
+ await bind(pub, true, "eq:bind:pool-buck-usdc");
180
+ const b0 = await session.call(poolUB, "token0");
181
+ const b1 = await session.call(poolUB, "token1");
182
+ const Lub = usdc.address.toLowerCase() === b0.toLowerCase()
183
+ ? (targetBuck * spU) / Q96 : (targetBuck * Q96) / spU;
184
+ const [loU, hiU] = fullRangeTicks(SPACING[FEES.ub]);
185
+ await session.send(simlp, "mint", [pub, loU, hiU, Lub, b0, b1],
186
+ { tag: "eq:seed:buck-usdc" });
187
+
188
+ // --- the real router, identity-bound ---------------------------------
189
+ const weth = await session.deploy(artifacts("WETH9"), [], { name: "WETH9", gas });
190
+ const router = await deployUniversalRouter(session, opts.urArtifact,
191
+ { weth: weth.address, v3Factory: v3f.address,
192
+ poolInitCode: artifacts("UniswapV3Pool").bytecode });
193
+ await bind(router.address, true, "eq:bind:router");
194
+
195
+ // --- reference feeds (the whales' walks; refUsd's ground truth) ------
196
+ const feeds = tokens.map((t, i) => seededWalk({
197
+ seed: (opts.feedSeed ?? 0xF00D5) + i, start: t.p0,
198
+ stepBp: opts.stepBp ?? 80, steps: opts.feedDays ?? 4000 }));
199
+
200
+ Object.assign(world, {
201
+ v3f, basket, usdc, simlp, tokens, poolUB, router, weth, feeds,
202
+ fees: FEES, series: [], _proxies: new Map(),
203
+ receipts: tokens.length, // the bootstrap deposits' receipts
204
+ });
205
+
206
+ // ==== agent-facing op helpers (the doctrine's plumbing layer) ========
207
+
208
+ const erc20At = (addr) => session.contractAt(erc20Art.abi, addr);
209
+ world.holderAddress = (account) =>
210
+ world._proxies.get(account.address)?.address ?? account.address;
211
+
212
+ world.spotUB = async () => spotFromSqrtPriceX96(
213
+ (await session.call(poolUB, "slot0"))[0], buck.address, 6, usdc.address);
214
+ world.spotUsd = async (i) => spotFromSqrtPriceX96(
215
+ (await session.call(tokens[i].poolUsdc, "slot0"))[0],
216
+ tokens[i].erc20.address, tokens[i].dec, usdc.address);
217
+ world.spotBuck = async (i) => spotFromSqrtPriceX96(
218
+ (await session.call(tokens[i].poolBuck, "slot0"))[0],
219
+ tokens[i].erc20.address, tokens[i].dec, buck.address);
220
+ world.bvib = async () => session.call(basket, "basketValueInBuck");
221
+ world.K = async () => session.call(kctrl, "buckK");
222
+ world.usdcForBuck = async (buckOut) =>
223
+ (buckOut * (await world.spotUB()) / 1_000_000n) * 102n / 100n;
224
+ world.refUsd = (i, day, amount) =>
225
+ (amount * feeds[i][day % feeds[i].length]) / 10n ** BigInt(tokens[i].dec);
226
+
227
+ /** The fiat bridge in: income, endowments (MockERC20 mint). */
228
+ world.fiatIn = (account, amount, { tag } = {}) =>
229
+ session.send(usdc, "mint", [world.holderAddress(account), amount],
230
+ { tag: tag ?? "eq:fiat-in" });
231
+
232
+ /** Lazy per-holder proxy: SimLP instance, bound public+NON-carrying
233
+ * (carrying accounts cannot draw credit negative). */
234
+ world.proxyFor = async (account) => {
235
+ let p = world._proxies.get(account.address);
236
+ if (!p) {
237
+ p = await session.deploy(artifacts("SimLP"), [],
238
+ { name: "HolderProxy", gas });
239
+ await bind(p.address, false, "eq:bind:proxy");
240
+ world._proxies.set(account.address, p);
241
+ }
242
+ return p;
243
+ };
244
+
245
+ /** Pledge insured assets: BuckCredit to the holder's proxy + activate
246
+ * the face as headroom (zero premium: the ff gate is inapplicable). */
247
+ world.pledge = async (account, face, { tag } = {}) => {
248
+ const p = await world.proxyFor(account);
249
+ const ts = (await session.client.getBlock()).timestamp;
250
+ await session.send(credit, "createCredit",
251
+ [p.address, 0, face, 0n, 0, 0, ts, 0], { tag: `${tag}:credit` });
252
+ await session.send(p, "exec",
253
+ [buck.address, encodeFunctionData(
254
+ { abi: buck.abi, functionName: "mint", args: [face] })],
255
+ { tag: `${tag}:activate`, gas: 3_000_000n });
256
+ return p;
257
+ };
258
+
259
+ // ==== insured-credit ops: the AUDITED debtor's plumbing ==============
260
+ //
261
+ // The honest issuance channel proven by the Python ledger audit
262
+ // (test_debtor_ledger.py): premium-bearing credits make Buck.mint's
263
+ // funding-factor gate REAL, tranches activate against unactivated
264
+ // face, the liability is quoted net of BuckCredit's Jubilee aging,
265
+ // and the unwind bites only below USD par, sized so the buy itself
266
+ // cannot lift the pool past par.
267
+
268
+ world._credits = new Map();
269
+
270
+ /** Pledge insured assets as PREMIUM-BEARING credits (no upfront mint:
271
+ * tranches activate later through the live funding gate). */
272
+ world.pledgeInsured = async (account, faces, premiumBp, { tag } = {}) => {
273
+ const p = await world.proxyFor(account);
274
+ const ts = (await session.client.getBlock()).timestamp;
275
+ for (const face of faces) {
276
+ await session.send(credit, "createCredit",
277
+ [p.address, 0, face, 0n, 0, 0, ts, Number(premiumBp)],
278
+ { tag: `${tag}:credit` });
279
+ }
280
+ const ids = [];
281
+ for (let i = 0; i < faces.length; i++) {
282
+ ids.push(await session.call(credit, "tokenOfOwnerByIndex",
283
+ [p.address, BigInt(i)]));
284
+ }
285
+ world._credits.set(account.address, ids);
286
+ return p;
287
+ };
288
+
289
+ /** The debtor's chain truth in one read: drawn / limit / held /
290
+ * unactivated face / the Jubilee relief quote (liability melts). */
291
+ world.creditState = async (account) => {
292
+ const me = world.holderAddress(account);
293
+ const ids = world._credits.get(account.address) ?? [];
294
+ const [signed, limit] = await Promise.all([
295
+ session.call(buck, "signedBalanceOf", [me]),
296
+ session.call(buck, "creditLimit", [me])]);
297
+ let unactivated = 0n;
298
+ let jub = 0n;
299
+ for (const tid of ids) {
300
+ const info = await session.call(credit, "creditInfo", [tid]);
301
+ const [face, act] = [info[0], info[1]];
302
+ unactivated += face > act ? face - act : 0n;
303
+ jub += await session.call(credit, "jubileeRelief", [tid]);
304
+ }
305
+ const drawn = signed < 0n ? -signed : 0n;
306
+ return { drawn, limit, held: signed > 0n ? signed : 0n, unactivated, jub,
307
+ headroom: (limit > drawn ? limit - drawn : 0n) + unactivated };
308
+ };
309
+
310
+ /** What the funding gate demands for the next tranche: quoteMint's
311
+ * insurance principal scaled by the live fundingFactor, less what
312
+ * the holder's balanceOf already covers. */
313
+ world.gateShortfall = async (account, tranche) => {
314
+ const me = world.holderAddress(account);
315
+ const ids = world._credits.get(account.address) ?? [];
316
+ const quote = await session.call(buck, "quoteMint", [tranche, ids]);
317
+ const ff = await session.call(kctrl, "fundingFactor");
318
+ const required = (quote[1] * ff) / E18;
319
+ const bal = await session.call(buck, "balanceOf", [me]);
320
+ return { required, shortfall: required > bal ? required - bal : 0n };
321
+ };
322
+
323
+ /** Activate a tranche through the REAL gate. Returns {ok, premium}
324
+ * -- premium is the insurance principal drawn (signed delta); a
325
+ * revert is the gate saying "save more" (the caller's throttle). */
326
+ world.mintTranche = async (account, amount, { tag } = {}) => {
327
+ const p = world._proxies.get(account.address);
328
+ const before = await session.call(buck, "signedBalanceOf", [p.address]);
329
+ const rcpt = await session.send(p, "exec",
330
+ [buck.address, encodeFunctionData(
331
+ { abi: buck.abi, functionName: "mint", args: [amount] })],
332
+ { tag, gas: 3_000_000n, expect: "either" });
333
+ if (rcpt.status !== "success") return { ok: false, premium: 0n };
334
+ const after = await session.call(buck, "signedBalanceOf", [p.address]);
335
+ return { ok: true, premium: before > after ? before - after : 0n };
336
+ };
337
+
338
+ /** Sell capped by the live spendable balance (held + unused credit). */
339
+ world.sellBuckCapped = async (buckIn, account, opts2 = {}) => {
340
+ const me = world.holderAddress(account);
341
+ const sp = await session.call(buck, "balanceOf", [me]);
342
+ const amt = buckIn < sp ? buckIn : sp;
343
+ if (amt < 10n ** 6n) return { sold: 0n, got: 0n };
344
+ return { sold: amt, got: await world.sellBuck(amt, account, opts2) };
345
+ };
346
+
347
+ /** The unwind bite: the pool's USDC spot and the largest BUCK buy
348
+ * that cannot lift it past par -- buying x of reserve r moves spot p
349
+ * to p*(r/(r-x))^2, which stays <= 1 for x <= r*(1-sqrt(p)). */
350
+ world.unwindBite = async () => {
351
+ const spot = await world.spotUB();
352
+ const reserve = await session.call(buck, "balanceOf", [poolUB.address]);
353
+ const E6 = 10n ** 6n;
354
+ if (spot >= E6) return { spot, capBuck: 0n };
355
+ const cap = (reserve * (E6 - isqrt(spot * E6))) / E6;
356
+ return { spot, capBuck: cap };
357
+ };
358
+
359
+ /** Swap along a [token, fee, token, ...] path through the REAL router
360
+ * (pre-fund route); returns the output-token delta at the holder. */
361
+ world.route = async (path, amountIn, account, { tag } = {}) => {
362
+ const holder = world.holderAddress(account);
363
+ const proxy = world._proxies.get(account.address);
364
+ const tokenIn = erc20At(path[0]);
365
+ const tokenOut = erc20At(path[path.length - 1]);
366
+ const before = await session.call(tokenOut, "balanceOf", [holder]);
367
+ if (proxy) {
368
+ await session.send(proxy, "exec",
369
+ [path[0], encodeFunctionData({ abi: erc20Art.abi,
370
+ functionName: "transfer", args: [router.address, amountIn] })],
371
+ { tag: `${tag}:fund` });
372
+ } else {
373
+ await session.send(tokenIn, "transfer", [router.address, amountIn],
374
+ { account, tag: `${tag}:fund` });
375
+ }
376
+ const [commands, inputs] = urExecArgs(holder, amountIn, encodePath(path));
377
+ await session.send(router, "execute", [commands, inputs],
378
+ { tag, gas: 1_500_000n });
379
+ return (await session.call(tokenOut, "balanceOf", [holder])) - before;
380
+ };
381
+
382
+ world.buyBuck = (usdcIn, account, opts2 = {}) => world.route(
383
+ [usdc.address, FEES.ub, buck.address], usdcIn, account, opts2);
384
+ world.sellBuck = (buckIn, account, opts2 = {}) => world.route(
385
+ [buck.address, FEES.ub, usdc.address], buckIn, account, opts2);
386
+
387
+ /** Buy TOKEN i with USDC via the better-quoted route (direct vs
388
+ * through BUCK), quoted client-side from spots + fees. */
389
+ world.buyTokenBest = async (i, usdcIn, account, opts2 = {}) => {
390
+ const t = tokens[i];
391
+ const unit = 10n ** BigInt(t.dec);
392
+ const [pTU, pUB, pTB] = await Promise.all(
393
+ [world.spotUsd(i), world.spotUB(), world.spotBuck(i)]);
394
+ const direct = (usdcIn * unit / pTU) * 9970n / 10000n;
395
+ const buckOut = (usdcIn * 1_000_000n / pUB) * 9995n / 10000n;
396
+ const viaBuck = (buckOut * unit / pTB) * 9970n / 10000n;
397
+ const path = viaBuck > direct
398
+ ? [usdc.address, FEES.ub, buck.address, FEES.buck, t.erc20.address]
399
+ : [usdc.address, FEES.usdc, t.erc20.address];
400
+ const out = await world.route(path, usdcIn, account, opts2);
401
+ return { out, viaBuck: viaBuck > direct };
402
+ };
403
+
404
+ /** Deposit TOKEN i into the basket (the basket mints BUCK and LPs the
405
+ * pair); returns {receiptId}. */
406
+ world.basketDeposit = async (i, amount, account, { tag } = {}) => {
407
+ const t = tokens[i];
408
+ await session.send(t.erc20, "approve", [basket.address, amount],
409
+ { account, tag: `${tag}:approve` });
410
+ const rcpt = await session.send(basket, "depositToken",
411
+ [t.erc20.address, amount, 0n],
412
+ { account, tag, gas: 3_000_000n });
413
+ const log = rcpt.logs.find((l) =>
414
+ l.address.toLowerCase() === basket.address.toLowerCase()
415
+ && l.topics[0] === DEPOSITED_TOPIC);
416
+ world.receipts += 1;
417
+ return { receiptId: BigInt(log.topics[2]) };
418
+ };
419
+
420
+ /** basketDeposit for proxy holders (credit-drawing debtors): the
421
+ * tokens live at the proxy, so approval and deposit exec through it;
422
+ * the receipt lands on the proxy. EOA holders fall through. */
423
+ world.basketDepositAs = async (i, amount, account, { tag } = {}) => {
424
+ const p = world._proxies.get(account.address);
425
+ if (!p) return world.basketDeposit(i, amount, account, { tag });
426
+ const t = tokens[i];
427
+ await session.send(p, "exec",
428
+ [t.erc20.address, encodeFunctionData({ abi: erc20Art.abi,
429
+ functionName: "approve", args: [basket.address, amount] })],
430
+ { tag: `${tag}:approve` });
431
+ const rcpt = await session.send(p, "exec",
432
+ [basket.address, encodeFunctionData({ abi: basket.abi,
433
+ functionName: "depositToken", args: [t.erc20.address, amount, 0n] })],
434
+ { tag, gas: 3_000_000n });
435
+ const log = rcpt.logs.find((l) =>
436
+ l.address.toLowerCase() === basket.address.toLowerCase()
437
+ && l.topics[0] === DEPOSITED_TOPIC);
438
+ world.receipts += 1;
439
+ return { receiptId: BigInt(log.topics[2]) };
440
+ };
441
+
442
+ /** Redeem a receipt in full; returns the USD value received (token at
443
+ * the day's spot + BUCK at the floating spot), 6-dec. */
444
+ world.basketRedeem = async (receiptId, account, { tag } = {}) => {
445
+ const holder = world.holderAddress(account);
446
+ const before = await Promise.all([
447
+ session.call(buck, "balanceOf", [holder]),
448
+ ...tokens.map((t) => session.call(t.erc20, "balanceOf", [holder]))]);
449
+ await session.send(basket, "redeem", [receiptId, 10_000n],
450
+ { account, tag, gas: 5_000_000n });
451
+ const after = await Promise.all([
452
+ session.call(buck, "balanceOf", [holder]),
453
+ ...tokens.map((t) => session.call(t.erc20, "balanceOf", [holder]))]);
454
+ let usd = (after[0] - before[0]) * (await world.spotUB()) / 1_000_000n;
455
+ for (let i = 0; i < tokens.length; i++) {
456
+ usd += (after[i + 1] - before[i + 1]) * (await world.spotUsd(i))
457
+ / 10n ** BigInt(tokens[i].dec);
458
+ }
459
+ return usd;
460
+ };
461
+
462
+ /** Mark a receipt to market: principals valued at today's references
463
+ * (v1: LP-fee drift inside the position is not counted). */
464
+ world.receiptValueUsd = async (receiptId, day) => {
465
+ const dep = await session.call(basket, "deposits", [receiptId]);
466
+ const [principalBuck, principalTok, tokenAddr] = [dep[0], dep[1], dep[2]];
467
+ const i = tokens.findIndex((t) =>
468
+ t.erc20.address.toLowerCase() === String(tokenAddr).toLowerCase());
469
+ let usd = principalBuck * (await world.spotUB()) / 1_000_000n;
470
+ if (i >= 0) usd += world.refUsd(i, day, principalTok);
471
+ return usd;
472
+ };
473
+
474
+ /** Sample the loop's observables into world.series (chart food):
475
+ * the controller pair (K, fundingFactor), the peg observable, and
476
+ * every pool's spot beside its reference. */
477
+ world.record = async (day) => {
478
+ const [K, bvib, spotUB, ff] = await Promise.all(
479
+ [world.K(), world.bvib(), world.spotUB(),
480
+ session.call(kctrl, "fundingFactor")]);
481
+ const spots = await Promise.all(tokens.map((_, i) => world.spotUsd(i)));
482
+ const spotsBuck = await Promise.all(tokens.map((_, i) => world.spotBuck(i)));
483
+ const refs = tokens.map((_, i) => feeds[i][day % feeds[i].length]);
484
+ world.series.push({ day, K, bvib, spotUB, ff, spots, spotsBuck, refs });
485
+ };
486
+
487
+ return world;
488
+ }
489
+
490
+ /** The clock: FIRST in the agent order, it advances simulated time one
491
+ * day per tick -- demurrage accrues, the PID integrates real dT. */
492
+ export class DayClock {
493
+ async act(world, day, tick) {
494
+ if (tick === 0) await advanceTime(world, DAY);
495
+ }
496
+ }
497
+
498
+ /** Advances the PID every tick -- compute() is permissionless and
499
+ * no-ops until dT has elapsed, exactly the Python PidKeeperAgent. */
500
+ export class PidKeeper {
501
+ async act(world, day, tick) {
502
+ await world.session.send(world.kctrl, "compute", [],
503
+ { tag: `pid:compute:d${day}` });
504
+ }
505
+ }
506
+
507
+ /** The fiat rail: a fixed USDC income minted to `account`'s holder
508
+ * every `everyDays` (agent order puts income before its spender). */
509
+ export class MonthlyIncome {
510
+ constructor({ account, amount, everyDays = 30 }) {
511
+ Object.assign(this, { account, amount, everyDays });
512
+ }
513
+ async act(world, day, tick) {
514
+ if (tick !== 0 || day % this.everyDays !== 0) return;
515
+ await world.fiatIn(this.account, this.amount, { tag: `income:d${day}` });
516
+ }
517
+ }
@@ -0,0 +1,35 @@
1
+ // The journal-parity mini-scenario, JS side: EXACTLY the op sequence
2
+ // alberta_buck.sim.mini emits (same tags, same order, same values) --
3
+ // buildOnePool, stock the trader, then per day a whale pin to the
4
+ // scenario's fixed target and a round-trip trade (with the day-0 SPL
5
+ // demo, declared expect:"revert"). core/vectors/mini-scenario.json is
6
+ // the single source of the numbers.
7
+
8
+ import { PinWhale } from "../agents/whale.js";
9
+ import { RoundTripTrader } from "../agents/trader.js";
10
+ import { runDays } from "../world.js";
11
+ import { buildOnePool } from "./onepool.js";
12
+
13
+ export async function runMini(session, artifacts, sc) {
14
+ const world = await buildOnePool(session, artifacts, {
15
+ price: BigInt(sc.price),
16
+ usdcDepth: BigInt(sc.usdcDepth),
17
+ });
18
+ const { token, usdc, pool, simlp } = world;
19
+
20
+ await session.send(token, "mint",
21
+ [session.account.address, BigInt(sc.traderStock)],
22
+ { tag: "mini:stock-trader" });
23
+
24
+ const whale = new PinWhale({
25
+ simlp, pool, token: token.address, tokenDec: sc.tokenDec,
26
+ quote: usdc.address,
27
+ refPrice: (day) => BigInt(sc.targets[day]),
28
+ });
29
+ const trader = new RoundTripTrader({
30
+ simlp, pool, token, quote: usdc, amount: BigInt(sc.tradeTokens),
31
+ });
32
+
33
+ await runDays(world, [whale, trader], { days: sc.days, ticksPerDay: 1 });
34
+ return world;
35
+ }
@@ -0,0 +1,59 @@
1
+ // buildOnePool: the smallest real world -- TOKEN and USDC (MockERC20s), a
2
+ // Uniswap V3 factory + one TOKEN/USDC pool initialized at `price`, and a
3
+ // SimLP helper stocked and LP'd full-range, mirroring the Python
4
+ // deploy.py's TOKEN/USDC pool construction. Runs on ANY session --
5
+ // Tevm in-process (standalone JS sim) or anvil -- from the same code.
6
+
7
+ import { Q96, fullRangeTicks, sqrtPriceX96 } from "../v3.js";
8
+
9
+ const FEE = 3000; // 60-tick spacing, as the sim's TOKEN/USDC pools
10
+ const TICK_SPACING = 60;
11
+
12
+ /**
13
+ * @param session a Session
14
+ * @param artifacts (name) => {abi, bytecode} (nodefs loadArtifact, or a
15
+ * bundled map in the browser)
16
+ * @param opts.price USDC base units (1e6) per whole TOKEN (default $2.50)
17
+ * @param opts.usdcDepth USDC base units of full-range depth (default $10M)
18
+ * @returns world {session, usdc, token, pool, simlp, factory}
19
+ */
20
+ export async function buildOnePool(session, artifacts,
21
+ { price = 2_500_000n,
22
+ usdcDepth = 10_000_000n * 10n ** 6n } = {}) {
23
+ const gas = 15_000_000n; // deploys fit tevm's default block gas limit
24
+ const usdc = await session.deploy(artifacts("MockERC20"),
25
+ ["USD Coin", "USDC", 6], { name: "USDC", gas });
26
+ const token = await session.deploy(artifacts("MockERC20"),
27
+ ["Test Token", "TOK", 18], { name: "TOK", gas });
28
+ const factory = await session.deploy(artifacts("UniswapV3Factory"), [],
29
+ { name: "UniswapV3Factory", gas });
30
+ const simlp = await session.deploy(artifacts("SimLP"), [],
31
+ { name: "SimLP", gas });
32
+
33
+ await session.send(factory, "createPool",
34
+ [token.address, usdc.address, FEE], { tag: "onepool:createPool" });
35
+ const poolAddr = await session.call(factory, "getPool",
36
+ [token.address, usdc.address, FEE]);
37
+ const pool = session.contractAt(artifacts("UniswapV3Pool").abi, poolAddr);
38
+
39
+ const sqrt = sqrtPriceX96(token.address, 10n ** 18n, usdc.address, price);
40
+ await session.send(pool, "initialize", [sqrt], { tag: "onepool:init" });
41
+
42
+ // Stock SimLP generously (it pays both mint and later swaps from its
43
+ // own balance), then mint full-range liquidity sized to usdcDepth.
44
+ const tokenStock = (4n * usdcDepth * 10n ** 18n) / price;
45
+ await session.send(usdc, "mint", [simlp.address, 4n * usdcDepth],
46
+ { tag: "onepool:stock-usdc" });
47
+ await session.send(token, "mint", [simlp.address, tokenStock],
48
+ { tag: "onepool:stock-token" });
49
+
50
+ const usdcIs0 = usdc.address.toLowerCase() < token.address.toLowerCase();
51
+ const L = usdcIs0 ? (usdcDepth * sqrt) / Q96 : (usdcDepth * Q96) / sqrt;
52
+ const [lo, hi] = fullRangeTicks(TICK_SPACING);
53
+ const [t0, t1] = usdcIs0 ? [usdc.address, token.address]
54
+ : [token.address, usdc.address];
55
+ await session.send(simlp, "mint",
56
+ [pool.address, lo, hi, L, t0, t1], { tag: "onepool:mint-liquidity" });
57
+
58
+ return { session, usdc, token, pool, simlp, factory };
59
+ }