@logswap/sdk 0.5.71 → 0.5.72

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/src/fpool.ts CHANGED
@@ -31,7 +31,7 @@
31
31
  */
32
32
 
33
33
  import { decodeAbiParameters, encodeAbiParameters, encodeFunctionData, erc20Abi, keccak256, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
34
- import { fPoolManagerAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
34
+ import { fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
35
35
 
36
36
  /**
37
37
  * One contract, one ABI (F version 12): `harvest`, `claim`, `retire`, `regenerate`,
@@ -72,14 +72,21 @@ export interface FPoolState {
72
72
  Q: bigint;
73
73
  /** Total exposure. Grows on mint, shrinks on burn; never moves on a swap. */
74
74
  L: bigint;
75
- /** Fixed fee, WAD. At or above `FEE_MIN` — a zero-fee basket is not constructible. */
76
- phi: bigint;
77
- /** The strike: θ at seeding, and the fees-only harvest bound. */
78
- theta0: bigint;
79
- /** θ = X − Q/L, the shared floor. Derived from the holdings, never stored. */
80
- theta: bigint;
75
+ /** The swap fee, WAD (`phi` before F version 25, decisions 059). At or above `FEE_MIN` — a zero-fee basket is not constructible. */
76
+ swapFee: bigint;
77
+ /** θ₀, the reference log floor: where the floor stood at the seed (re-based by a capital harvest and at each step); income is counted from it. `theta0` before F version 25. */
78
+ refLogFloor: bigint;
79
+ /** θ = X − Q/L, the log of the pool's current floor price: where the bid runs out. Derived, never stored. `theta` before F version 25. */
80
+ logFloor: bigint;
81
81
  /** X = Σ w_j x_j, the composite index. */
82
82
  compositeX: bigint;
83
+ /** This generation's income still in the pool, quote: L·(θ₀ − θ). */
84
+ unharvested: bigint;
85
+ /**
86
+ * The common cap (decisions 061): d = ln(cap price / floor price) in 1e-6 log units, from the key;
87
+ * 0 is uncapped. Leverage at the floor is 1/(1 − e^{−d}): {@link fPoolLeverageOfCap}.
88
+ */
89
+ cap: number;
83
90
  /** Σ = ∫ Σ_j w_j (dx_j)², accrued even though the fee is fixed — the kernel stays calculable. */
84
91
  bigSigma: bigint;
85
92
  /**
@@ -110,87 +117,69 @@ export interface FPoolState {
110
117
  totalSupply: bigint;
111
118
  /** What the pool holds of each base — the STATE; the mark `x` is derived from it. */
112
119
  reserves: bigint[];
120
+ /** The reserve every price reads: `reserves[j] + L_j·κ_j` on a capped pool (061), `reserves[j]` otherwise. */
121
+ virtualReserves: bigint[];
122
+ /** κ_j = e^{−μ_j}, WAD: each leg's phantom reserve per unit of its liquidity. 0 uncapped. */
123
+ kappa: bigint[];
113
124
  }
114
125
 
115
- /** Read a basket pool whole. Batched — viem multicalls these when the transport allows. */
116
- export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolState> {
117
- // viem types `functionName` as a literal union; this reader is deliberately generic, so the
118
- // cast lives here once rather than at each call site below.
119
- const rd = <T>(functionName: string, args: readonly unknown[]) =>
120
- c.public.readContract({
121
- address: c.addresses.fPoolManager!,
122
- abi: fPoolManagerAbi,
123
- functionName,
124
- args,
125
- } as never) as Promise<T>;
126
-
127
- // `getPool` returns the whole scalar struct in one call — the singleton's shape makes what used
128
- // to be a dozen getters a single read. No role in it since F version 15, no kind and no name
129
- // since 18: those are the policy's.
130
- const p = await rd<{
131
- quote: Address;
132
- phi: bigint;
133
- policy: Address;
134
- seeded: boolean;
135
- gen: number;
136
- n: number;
137
- restructureBlock: number;
138
- asks: number;
139
- Q: bigint;
140
- bigSigma: bigint;
141
- L: bigint;
142
- shares: bigint;
143
- theta0: bigint;
144
- }>("getPool", [poolId]);
126
+ /**
127
+ * The lens's raw F pool struct — what the manager's `getPool` returned. Since F version 25 the
128
+ * manager's typed views are behind `extsload` (decisions 062) and the lens reads them; one call.
129
+ */
130
+ async function rawFPool(c: LogswapClient, poolId: Hex) {
131
+ const [p] = await c.public.readContract({
132
+ address: c.addresses.lens,
133
+ abi: logswapLensAbi,
134
+ functionName: "getFPools",
135
+ args: [[poolId]],
136
+ });
137
+ if (!p || /^0x0{40}$/i.test(p.quote)) throw new Error(`logswap: no F pool with id ${poolId}`);
138
+ return p;
139
+ }
145
140
 
141
+ /** Read a basket pool whole: one lens call (`fPool`, F version 25) plus the policy's kind and name. */
142
+ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolState> {
143
+ const v = await c.public.readContract({
144
+ address: c.addresses.lens,
145
+ abi: logswapLensAbi,
146
+ functionName: "fPool",
147
+ args: [poolId],
148
+ });
149
+ const p = v.pool;
146
150
  // A pool that was never created reads as an all-zero struct rather than reverting, so the
147
151
  // absence has to be named here or every caller mistakes "no such pool" for "a pool holding
148
152
  // nothing". `quote` is the manager's own existence test (`_live`), and it is what the create
149
153
  // flow's resumability turns on: an initialized-but-unseeded pool MUST still read, so this
150
154
  // cannot be a check on `seeded`.
151
155
  if (/^0x0{40}$/i.test(p.quote)) throw new Error(`logswap: no F pool with id ${poolId}`);
152
-
153
- const n = Number(p.n);
154
- const idx = Array.from({ length: n }, (_, i) => BigInt(i));
155
- const [theta, compositeX, legs, kind, name] = await Promise.all([
156
- // before the seed there is no L and no floor: θ is X by convention (older deployments revert
157
- // with DivWadFailed, so do not ask them)
158
- p.L > 0n ? rd<bigint>("theta", [poolId]) : rd<bigint>("compositeX", [poolId]),
159
- rd<bigint>("compositeX", [poolId]),
160
- Promise.all(idx.map((i) => rd<readonly [Address, bigint, bigint, bigint]>("legOf", [poolId, i]))),
161
- fPoolKindOfPolicy(c, p.policy),
162
- fPoolNameOf(c, poolId, p.policy),
163
- ]);
164
-
165
- const bases = legs.map((l) => l[0]);
166
- const weights = legs.map((l) => l[1]);
167
- const x = legs.map((l) => l[2]); // derived by the manager: x_j = ln(w_j·L / R_j)
168
- const reserves = legs.map((l) => l[3]); // the state
169
- const { quote, Q, L, phi, theta0, bigSigma, policy, restructureBlock, seeded, gen } = p;
170
- const totalSupply = p.shares;
171
-
156
+ const [kind, name] = await Promise.all([fPoolKindOfPolicy(c, p.policy), fPoolNameOf(c, poolId, p.policy)]);
172
157
  return {
173
158
  poolId,
174
- quote,
175
- bases,
176
- weights,
177
- x,
178
- Q,
179
- L,
180
- phi,
181
- theta0,
182
- theta,
183
- compositeX,
184
- bigSigma,
185
- policy,
159
+ quote: p.quote,
160
+ bases: v.legs.map((l) => l.base),
161
+ weights: v.legs.map((l) => l.weight),
162
+ x: v.legs.map((l) => l.x), // derived by the manager: x_j = ln(w_j·L / R̃_j)
163
+ Q: p.Q,
164
+ L: p.L,
165
+ swapFee: p.swapFee,
166
+ refLogFloor: p.refLogFloor,
167
+ logFloor: v.logFloor,
168
+ compositeX: v.compositeX,
169
+ unharvested: v.unharvested,
170
+ cap: Number(p.cap),
171
+ bigSigma: p.bigSigma,
172
+ policy: p.policy,
186
173
  asks: Number(p.asks),
187
174
  name,
188
- restructureBlock: Number(restructureBlock),
175
+ restructureBlock: Number(p.restructureBlock),
189
176
  kind,
190
- seeded,
191
- gen: Number(gen),
192
- totalSupply,
193
- reserves,
177
+ seeded: p.seeded,
178
+ gen: Number(p.gen),
179
+ totalSupply: p.shares,
180
+ reserves: v.legs.map((l) => l.reserve), // the state
181
+ virtualReserves: v.legs.map((l) => l.virtualReserve),
182
+ kappa: v.legs.map((l) => l.kappa),
194
183
  };
195
184
  }
196
185
 
@@ -248,14 +237,7 @@ export function fPoolPolicyFor(c: LogswapClient, kind: FPoolKind): Address {
248
237
 
249
238
  /** The policy a pool named in its key — one read; {@link getFPool} carries it too. */
250
239
  export async function fPoolPolicyOf(c: LogswapClient, poolId: Hex): Promise<Address> {
251
- const p = (await c.public.readContract({
252
- address: c.addresses.fPoolManager!,
253
- abi: fPoolManagerAbi,
254
- functionName: "getPool",
255
- args: [poolId],
256
- } as never)) as { quote: Address; policy: Address };
257
- if (/^0x0{40}$/i.test(p.quote)) throw new Error(`logswap: no F pool with id ${poolId}`);
258
- return p.policy;
240
+ return (await rawFPool(c, poolId)).policy;
259
241
  }
260
242
 
261
243
  /**
@@ -329,19 +311,11 @@ export interface FPoolDescription {
329
311
  }
330
312
 
331
313
  export async function describeFPool(c: LogswapClient, s: FPoolState): Promise<FPoolDescription> {
332
- const rd = <T>(functionName: string, args: readonly unknown[] = [s.poolId]) =>
333
- c.public.readContract({
334
- address: c.addresses.fPoolManager!,
335
- abi: fPoolManagerAbi,
336
- functionName,
337
- args,
338
- } as never) as Promise<T>;
339
314
  // an unseeded pool has no L: nothing earned, nothing to harvest, no pot. The bid is the
340
315
  // manager's ceiling (`harvestable()` until F version 21, which cut the getter): Q once seeded.
341
316
  const harvestable = s.L > 0n ? s.Q : 0n;
342
- const [unharvested, pot] = s.L > 0n
343
- ? await Promise.all([rd<bigint>("unharvested"), rd<bigint>("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
344
- : [0n, 0n];
317
+ const unharvested = s.L > 0n ? s.unharvested : 0n;
318
+ const pot = s.L > 0n ? await fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, s.gen) : 0n;
345
319
  return {
346
320
  shape: s.kind === "pad" ? "launch": "basket", // the kind, not n = 1
347
321
  legs: s.bases.length,
@@ -357,10 +331,22 @@ export function fPoolPriceOf(x: bigint, scale = 1): number {
357
331
  return Math.exp(Number(x) / 1e18) * scale;
358
332
  }
359
333
 
360
- /** Value per share at the pool's own marks. `V = L + Q`, so this is just that, pro-rated. */
334
+ /**
335
+ * Value per share at the pool's own marks, WAD. Uncapped, `V = L + Q`. Capped (decisions 061), leg
336
+ * j's base is worth `L_j·R_j/R̃_j`, less than `L_j` by the phantom the cap removed — the lens's
337
+ * `shareValue`, without the call.
338
+ */
361
339
  export function fPoolShareValue(s: FPoolState): bigint {
362
340
  if (s.totalSupply === 0n) return 0n;
363
- return ((s.L + s.Q) * WAD) / s.totalSupply;
341
+ let v = s.L + s.Q;
342
+ if (s.cap !== 0) {
343
+ v = s.Q;
344
+ for (let j = 0; j < s.reserves.length; j++) {
345
+ const rv = s.virtualReserves[j]!;
346
+ if (rv !== 0n) v += (((s.weights[j]! * s.L) / WAD) * s.reserves[j]!) / rv;
347
+ }
348
+ }
349
+ return (v * WAD) / s.totalSupply;
364
350
  }
365
351
 
366
352
  // ─── swaps ────────────────────────────────────────────────────────────────────
@@ -624,15 +610,84 @@ export async function fPoolClaim(c: LogswapClient, a: { poolId: Hex; gen?: bigin
624
610
  return writeFPool(c, a.poolId, "claim", [BigInt(gen), a.to ?? a.account], a.account);
625
611
  }
626
612
 
627
- /** A holder's dividend as of now on generation `gen` (the live one by default) — banked plus pending on the current balance. */
613
+ /**
614
+ * A holder's claimable dividend as of now on generation `gen` (the live one by default) — banked
615
+ * plus pending on the current balance; under a lien, the free fraction's only: the locked
616
+ * fraction's is the lien's ({@link fPoolLienOf}, decisions 063).
617
+ */
628
618
  export async function fPoolDividendOf(c: LogswapClient, poolId: Hex, holder: Address, gen?: bigint | number): Promise<bigint> {
629
619
  const g = gen ?? (await fPoolGen(c, poolId));
630
620
  return c.public.readContract({
631
- address: c.addresses.fPoolManager!,
632
- abi: fPoolManagerAbi,
633
- functionName: "dividendOf",
621
+ address: c.addresses.lens,
622
+ abi: logswapLensAbi,
623
+ functionName: "fDividendOf",
634
624
  args: [poolId, BigInt(g), holder],
635
- } as never) as Promise<bigint>;
625
+ });
626
+ }
627
+
628
+ // ─── the lien (decisions 063) ─────────────────────────────────────────────────
629
+ // A lending contract — the LOCKER — locks part of an owner's shares in place: no transfer, burn or
630
+ // redeem past the free balance. The dividend the locked amount earns is HELD BY THE LIEN, claimable
631
+ // by nobody while it stands; lifted, it goes with the lifted part — to the owner on a release (a
632
+ // repaid loan gets it back), to the seizer on a seizure. A borrower frozen out by `regenerate` keeps
633
+ // its lien on the old generation's id.
634
+
635
+ /** A lien on one share id: the locker, the amount locked, and the dividend the lien holds (banked). */
636
+ export interface FPoolLien {
637
+ locker: Address;
638
+ amount: bigint;
639
+ owed: bigint;
640
+ }
641
+
642
+ /**
643
+ * The lien on `owner`'s shares of `poolId`'s generation `gen` (the live one by default). `owed` counts
644
+ * what has been banked into it; the locked fraction of the owner's pending since its last bank is
645
+ * not yet in it — {@link fPoolBankLien} banks it.
646
+ */
647
+ export async function fPoolLienOf(c: LogswapClient, poolId: Hex, owner: Address, gen?: bigint | number): Promise<FPoolLien> {
648
+ const g = gen ?? (await fPoolGen(c, poolId));
649
+ const [locker, amount, owed] = await c.public.readContract({
650
+ address: c.addresses.lens,
651
+ abi: logswapLensAbi,
652
+ functionName: "fLienOf",
653
+ args: [fPoolShareId(poolId, g), owner],
654
+ });
655
+ return { locker, amount, owed };
656
+ }
657
+
658
+ /**
659
+ * Lock `amount` of `owner`'s shares for the caller, the locker (`account`). Paid with the owner's
660
+ * per-id allowance to the locker — the owner first sends `approve(locker, shareId, amount)` on the
661
+ * F manager ({@link fPoolApproveShares}) — or an operator grant. One locker per (id, owner) while a
662
+ * lien stands. In practice the lending contract calls `lock` itself; this is the EOA path.
663
+ */
664
+ export async function fPoolLock(c: LogswapClient, a: { poolId: Hex; owner: Address; amount: bigint; gen?: bigint | number; account: Address }) {
665
+ const g = a.gen ?? (await fPoolGen(c, a.poolId));
666
+ return writeFPool0(c, "lock", [fPoolShareId(a.poolId, g), a.owner, a.amount], a.account);
667
+ }
668
+
669
+ /**
670
+ * The locker's `unlock`: `amount` of the lien lifted with its share of the lien's dividend, credited
671
+ * to `to` — the owner (a release) or anyone else (a seizure, the shares moving to `to`). It banks
672
+ * first. The locker's alone.
673
+ */
674
+ export async function fPoolUnlock(
675
+ c: LogswapClient,
676
+ a: { poolId: Hex; owner: Address; amount: bigint; to: Address; gen?: bigint | number; account: Address },
677
+ ) {
678
+ const g = a.gen ?? (await fPoolGen(c, a.poolId));
679
+ return writeFPool0(c, "unlock", [fPoolShareId(a.poolId, g), a.owner, a.amount, a.to], a.account);
680
+ }
681
+
682
+ /** Bank the owner's pending into the lien now — `unlock(id, owner, 0, owner)`: dividends bank lazily. The locker's. */
683
+ export async function fPoolBankLien(c: LogswapClient, a: { poolId: Hex; owner: Address; gen?: bigint | number; account: Address }) {
684
+ return fPoolUnlock(c, { ...a, amount: 0n, to: a.owner });
685
+ }
686
+
687
+ /** The owner's per-id allowance to a locker on the F manager's ERC-6909 shares — what `lock` spends. */
688
+ export async function fPoolApproveShares(c: LogswapClient, a: { poolId: Hex; spender: Address; amount: bigint; gen?: bigint | number; account: Address }) {
689
+ const g = a.gen ?? (await fPoolGen(c, a.poolId));
690
+ return writeFPool0(c, "approve", [a.spender, fPoolShareId(a.poolId, g), a.amount], a.account);
636
691
  }
637
692
 
638
693
  // ─── internals ────────────────────────────────────────────────────────────────
@@ -788,13 +843,7 @@ export function fPoolGenOfShareId(id: bigint): number {
788
843
 
789
844
  /** The pool's live generation, one read. */
790
845
  export async function fPoolGen(c: LogswapClient, poolId: Hex): Promise<number> {
791
- const p = (await c.public.readContract({
792
- address: c.addresses.fPoolManager!,
793
- abi: fPoolManagerAbi,
794
- functionName: "getPool",
795
- args: [poolId],
796
- } as never)) as { gen: number };
797
- return Number(p.gen);
846
+ return Number((await rawFPool(c, poolId)).gen);
798
847
  }
799
848
 
800
849
  /** The ERC-6909 id carrying claims on a token. Bit 255 clear, so it can never meet a share id. */
@@ -819,12 +868,12 @@ export async function fPoolBundleOf(
819
868
  poolId: Hex,
820
869
  gen: bigint | number,
821
870
  ): Promise<{ supply: bigint; quote: bigint; base: bigint[] }> {
822
- const [supply, quote, base] = (await c.public.readContract({
823
- address: c.addresses.fPoolManager!,
824
- abi: fPoolManagerAbi,
825
- functionName: "bundleOf",
871
+ const [supply, quote, base] = await c.public.readContract({
872
+ address: c.addresses.lens,
873
+ abi: logswapLensAbi,
874
+ functionName: "fBundleOf",
826
875
  args: [poolId, BigInt(gen)],
827
- } as never)) as readonly [bigint, bigint, readonly bigint[]];
876
+ });
828
877
  return { supply, quote, base: [...base] };
829
878
  }
830
879
 
@@ -945,8 +994,13 @@ export interface FPoolCreateArgs {
945
994
  bases: Address[];
946
995
  /** WAD each; must sum to 1e18 — validated here so the revert is readable. */
947
996
  weights: bigint[];
948
- /** Fixed fee, WAD. The base-for-base fee is pinned at 2φ on-chain. */
949
- phi: bigint;
997
+ /** The swap fee, WAD (`phi` before F version 25). The base-for-base fee is pinned at 2φ on-chain. A curated policy admits only its listed tiers (decisions 059). */
998
+ swapFee: bigint;
999
+ /**
1000
+ * The common cap (decisions 061): d = ln(cap price / floor price) in 1e-6 log units, a key field;
1001
+ * 0 or absent is uncapped. From a leverage λ: {@link fPoolCapOfLeverage}.
1002
+ */
1003
+ cap?: number;
950
1004
  /**
951
1005
  * Which of the protocol's four the pool is — see {@link FPoolKind}. NOT a key field (decisions
952
1006
  * 049: the manager knows no kind): it selects the deployment's policy of that kind when `policy`
@@ -1006,11 +1060,13 @@ export interface FPoolKey {
1006
1060
  bases: Address[];
1007
1061
  /** WAD, aligned with `bases`, summing to 1e18. */
1008
1062
  weights: bigint[];
1009
- phi: bigint;
1063
+ swapFee: bigint;
1010
1064
  /** The policy AT BIRTH — not the live one after a move. */
1011
1065
  policy: Address;
1012
1066
  creator: Address;
1013
1067
  salt: Hex;
1068
+ /** The common cap, d in 1e-6 log units; 0 uncapped (decisions 061). Appended in F version 25: every pool id moved. */
1069
+ cap: number;
1014
1070
  }
1015
1071
 
1016
1072
  const F_POOL_KEY_ABI = [
@@ -1020,10 +1076,11 @@ const F_POOL_KEY_ABI = [
1020
1076
  { name: "quote", type: "address" },
1021
1077
  { name: "bases", type: "address[]" },
1022
1078
  { name: "weights", type: "uint256[]" },
1023
- { name: "phi", type: "uint256" },
1079
+ { name: "swapFee", type: "uint256" },
1024
1080
  { name: "policy", type: "address" },
1025
1081
  { name: "creator", type: "address" },
1026
1082
  { name: "salt", type: "bytes32" },
1083
+ { name: "cap", type: "uint24" },
1027
1084
  ],
1028
1085
  },
1029
1086
  ] as const;
@@ -1044,10 +1101,11 @@ export function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">
1044
1101
  quote: a.quote,
1045
1102
  bases,
1046
1103
  weights: companions[0]!,
1047
- phi: a.phi,
1104
+ swapFee: a.swapFee,
1048
1105
  policy: a.policy ?? fPoolPolicyFor(c, a.kind),
1049
1106
  creator: a.creator,
1050
1107
  salt: a.salt ?? ZERO_SALT,
1108
+ cap: a.cap ?? 0,
1051
1109
  };
1052
1110
  }
1053
1111
 
@@ -1094,21 +1152,22 @@ export async function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts: { salt
1094
1152
  });
1095
1153
  const l = logs[0];
1096
1154
  if (!l) throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
1097
- const a = l.args as { quote: Address; creator: Address; bases: readonly Address[]; weights: readonly bigint[]; phi: bigint; policy: Address };
1155
+ const a = l.args as { quote: Address; creator: Address; bases: readonly Address[]; weights: readonly bigint[]; swapFee: bigint; policy: Address; cap: number };
1098
1156
  const key: FPoolKey = {
1099
1157
  quote: a.quote,
1100
1158
  bases: [...a.bases],
1101
1159
  weights: [...a.weights],
1102
- phi: a.phi,
1160
+ swapFee: a.swapFee,
1103
1161
  policy: a.policy,
1104
1162
  creator: a.creator,
1105
1163
  salt: opts.salt ?? ZERO_SALT,
1164
+ cap: Number(a.cap),
1106
1165
  };
1107
1166
  if (fPoolIdOfKey(key).toLowerCase() !== poolId.toLowerCase() && opts.salt === undefined) {
1108
1167
  // a pad's salt carries its split (decisions 056): rebuild it from the policy's record with the default nonce
1109
1168
  try {
1110
- const [creatorShare, burnPerRung] = (await c.public.readContract({ address: a.policy, abi: padPolicyAbi, functionName: "splitOf", args: [poolId] } as never)) as readonly [bigint, bigint, bigint];
1111
- const padKey = { ...key, salt: padSalt({ creatorShare, burnPerRung }) };
1169
+ const [creatorShare, burnShare] = (await c.public.readContract({ address: a.policy, abi: padPolicyAbi, functionName: "splitOf", args: [poolId] } as never)) as readonly [bigint, bigint, bigint];
1170
+ const padKey = { ...key, salt: padSalt({ creatorShare, burnShare }) };
1112
1171
  if (fPoolIdOfKey(padKey).toLowerCase() === poolId.toLowerCase()) return padKey;
1113
1172
  } catch {
1114
1173
  /* not a pad: fall through to the error */
@@ -1127,11 +1186,38 @@ export async function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts: { salt
1127
1186
  * precision (~1e-16 relative) is what a seed needs: any reserve is a legal seed, and the mark
1128
1187
  * the pool derives from it is the one the sponsor meant to within that.
1129
1188
  */
1130
- export function fPoolReserveAt(Lj: bigint, x: bigint): bigint {
1131
- const e = Math.exp(-Number(x) / 1e18);
1189
+ export function fPoolReserveAt(Lj: bigint, x: bigint, k = 0): bigint {
1190
+ // on a capped pool (decisions 061) the mark reads the virtual reserve R̃ = R + c, and the seed
1191
+ // puts c = R·k/(1 − k): so the REAL reserve for a mark is L_j·e^{−x}·(1 − k)
1192
+ const e = Math.exp(-Number(x) / 1e18) * (1 - k);
1132
1193
  return (Lj * BigInt(Math.round(e * 1e18))) / 10n ** 18n;
1133
1194
  }
1134
1195
 
1196
+ /**
1197
+ * The seed's cap constant k = e^{Q0/L0 − d} (decisions 061): every leg's phantom is c_j = r_j·k/(1 − k).
1198
+ * 0 on an uncapped pool. Throws when the seed would be priced at or above its cap (Q0/L0 ≥ d), which
1199
+ * the manager refuses.
1200
+ */
1201
+ export function fPoolCapK(cap: number, Q0: bigint, L0: bigint): number {
1202
+ if (cap === 0) return 0;
1203
+ const e = Number((Q0 * WAD) / L0) / 1e18 - cap / 1e6;
1204
+ if (e >= 0) throw new Error(`logswap: a seed with Q0/L0 = ${Number((Q0 * WAD) / L0) / 1e18} is at or above its cap d = ${cap / 1e6}`);
1205
+ return Math.exp(e);
1206
+ }
1207
+
1208
+ /** The cap d (1e-6 log units) for a leverage λ > 1 at the floor: d = ln(λ/(λ − 1)) (decisions 061). λ = 2 puts the cap at twice the floor. */
1209
+ export function fPoolCapOfLeverage(lambda: number): number {
1210
+ if (!(lambda > 1)) throw new Error("logswap: leverage must exceed 1 (1 is uncapped: cap 0)");
1211
+ const d = Math.round(Math.log(lambda / (lambda - 1)) * 1e6);
1212
+ if (d < 1 || d > 0xffffff) throw new Error(`logswap: leverage ${lambda} needs a cap outside a uint24 of 1e-6 units`);
1213
+ return d;
1214
+ }
1215
+
1216
+ /** The leverage at the floor λ = 1/(1 − e^{−d}) for a cap d in 1e-6 log units; Infinity is impossible, 1 is uncapped. */
1217
+ export function fPoolLeverageOfCap(cap: number): number {
1218
+ return cap === 0 ? 1 : 1 / (1 - Math.exp(-cap / 1e6));
1219
+ }
1220
+
1135
1221
  /**
1136
1222
  * Arm the pool: deposit `r0[j]` of each base plus `Q0` of quote against exposure `L0`. Pass either
1137
1223
  * the reserves outright (`r0`) or the marks the pool should open at (`x0`, converted here with
@@ -1143,7 +1229,12 @@ export async function fPoolSeed(
1143
1229
  c: LogswapClient,
1144
1230
  a: { poolId: Hex; L0: bigint; Q0: bigint; account: Address } & ({ r0: bigint[] } | { x0: bigint[]; weights: bigint[] }),
1145
1231
  ) {
1146
- const r0 = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / 10n ** 18n, x));
1232
+ let r0: bigint[];
1233
+ if ("r0" in a) r0 = a.r0;
1234
+ else {
1235
+ const k = fPoolCapK(Number((await rawFPool(c, a.poolId)).cap), a.Q0, a.L0);
1236
+ r0 = a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / 10n ** 18n, x, k));
1237
+ }
1147
1238
  return writeFPool(c, a.poolId, "seed", [a.L0, r0, a.Q0], a.account);
1148
1239
  }
1149
1240
 
@@ -1205,8 +1296,9 @@ export async function fPoolEmptyingHarvests(c: LogswapClient, poolId: Hex): Prom
1205
1296
  * and redeems it with {@link fPoolRedeem} / rolls it with {@link fPoolRollIn} (`gen: g`); their
1206
1297
  * dividends on `g` stay claimable ({@link fPoolClaim} with `gen: g`). {@link fPoolClaimsOf}
1207
1298
  * lists them;
1208
- * - the pot: `toPayee` to the payee, `protocolCut` to the collector, the rest bought in as dead
1209
- * shares of `g + 1`; then the burn.
1299
+ * - the pot, all in quote (decisions 057): `toPayee` to the payee, `protocolCut` to the collector,
1300
+ * `burn` spent buying base at the curve and burning it (the price up, the floor held), and the
1301
+ * rest into the bid (the floor down). No share is minted; L does not move.
1210
1302
  *
1211
1303
  * A pad's gate is `threshold(gen)` (`PadPolicy.NotGraduated` below it); POL and private carry the
1212
1304
  * authority and send it the whole pot; a public pool refuses (`Refused`). A carried holder with no
@@ -1242,8 +1334,10 @@ export async function fPoolRelaunch(
1242
1334
  account: Address;
1243
1335
  } & ({ r0: bigint[] } | { x0: bigint[]; weights: bigint[] }),
1244
1336
  ) {
1245
- const [gen, harvests] = await Promise.all([fPoolGen(c, a.poolId), fPoolEmptyingHarvests(c, a.poolId)]);
1246
- const r0 = "r0" in a ? a.r0 : a.x0.map((x, j) => (a.weights[j]! === 0n ? 0n : fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x)));
1337
+ const [raw, harvests] = await Promise.all([rawFPool(c, a.poolId), fPoolEmptyingHarvests(c, a.poolId)]);
1338
+ const gen = Number(raw.gen);
1339
+ const k = "r0" in a ? 0 : fPoolCapK(Number(raw.cap), a.Q0, a.L0);
1340
+ const r0 = "r0" in a ? a.r0 : a.x0.map((x, j) => (a.weights[j]! === 0n ? 0n : fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x, k)));
1247
1341
  const enc = (functionName: string, args: unknown[]) =>
1248
1342
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
1249
1343
  const calls: Hex[] = [
@@ -1334,11 +1428,12 @@ export function fPoolLaunchCalls(
1334
1428
  ): Hex[] {
1335
1429
  const sum = a.weights.reduce((x, y) => x + y, 0n);
1336
1430
  if (sum !== WAD) throw new Error(`logswap: weights sum to ${sum}, expected 1e18`);
1337
- const given = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x));
1431
+ const k = "r0" in a ? 0 : fPoolCapK(a.cap ?? 0, a.Q0, a.L0);
1432
+ const given = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x, k));
1338
1433
  if (given.length !== a.bases.length) throw new Error(`logswap: ${given.length} reserves for ${a.bases.length} legs`);
1339
1434
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights, given);
1340
1435
  if (!a.policy) throw new Error("logswap: fPoolLaunchCalls needs the key's policy — fPoolLaunch fills it from the deployment");
1341
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, policy: a.policy, creator: a.creator, salt: a.salt ?? ZERO_SALT };
1436
+ const key = { quote: a.quote, bases, weights: companions[0]!, swapFee: a.swapFee, policy: a.policy, creator: a.creator, salt: a.salt ?? ZERO_SALT, cap: a.cap ?? 0 };
1342
1437
  const enc = (functionName: string, args: unknown[]) =>
1343
1438
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
1344
1439
  const calls: Hex[] = [];
@@ -1703,7 +1798,9 @@ export interface DiscoveredFPool {
1703
1798
  quote: Address;
1704
1799
  bases: Address[];
1705
1800
  weights: bigint[];
1706
- phi: bigint;
1801
+ swapFee: bigint;
1802
+ /** The common cap, d in 1e-6 log units; 0 uncapped (decisions 061). */
1803
+ cap: number;
1707
1804
  kind: FPoolKind;
1708
1805
  /** The pool's policy AT BIRTH — the contract the key names. The LIVE one is {@link getFPool}'s `policy`: the registry's owner may have moved the pool (046, `PolicyMigrated`). */
1709
1806
  policy: Address;
@@ -1727,7 +1824,7 @@ export interface DiscoveredFPool {
1727
1824
  /**
1728
1825
  * Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
1729
1826
  * policy, cached. Possible only because the event carries the full key preimage (bases,
1730
- * weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
1827
+ * weights, swapFee, policy, cap) — the poolId is a hash and could never be inverted; contracts PR #17
1731
1828
  * exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
1732
1829
  * a scan from block 0 per call is what made the first load wait on the node.
1733
1830
  */
@@ -1754,7 +1851,7 @@ export async function discoverFPools(
1754
1851
  logs.map(async (l) => {
1755
1852
  const a = l.args as {
1756
1853
  poolId: Hex; quote: Address; creator: Address; bases: readonly Address[];
1757
- weights: readonly bigint[]; phi: bigint; policy: Address;
1854
+ weights: readonly bigint[]; swapFee: bigint; policy: Address; cap: number;
1758
1855
  };
1759
1856
  const q0 = q0Of.get(a.poolId.toLowerCase());
1760
1857
  const kind = await fPoolKindOfPolicy(c, a.policy); // one read per distinct policy, cached
@@ -1763,7 +1860,8 @@ export async function discoverFPools(
1763
1860
  quote: a.quote,
1764
1861
  bases: [...a.bases],
1765
1862
  weights: [...a.weights],
1766
- phi: a.phi,
1863
+ swapFee: a.swapFee,
1864
+ cap: Number(a.cap ?? 0),
1767
1865
  kind,
1768
1866
  policy: a.policy,
1769
1867
  creator: a.creator,
@@ -1802,10 +1900,10 @@ export type FPoolRules =
1802
1900
  /** The pot the next graduation needs: `PadPolicy.threshold(gen)`. */
1803
1901
  threshold: bigint;
1804
1902
  pot: bigint;
1805
- /** This pad's split, recorded at its birth from the key's salt and never changed (decisions 056): the creator's cut of the pot, the protocol's, and the burn per rung. The rest of the pot is bought in. */
1903
+ /** This pad's split, recorded at its birth from the key's salt and never changed (decisions 056, 057): the creator's cut of the pot, the protocol's, and the burn — all fractions of the pot. The rest deepens the bid. */
1806
1904
  creatorShare: bigint;
1807
1905
  protocolShare: bigint;
1808
- burnPerRung: bigint;
1906
+ burnShare: bigint;
1809
1907
  /** The ranges the collector's policy holds NOW for the pads born after this read: what a new creator may choose. */
1810
1908
  ranges: PadRanges;
1811
1909
  }
@@ -1813,7 +1911,7 @@ export type FPoolRules =
1813
1911
  | { kind: "public"; curated: boolean }
1814
1912
  | { kind: "private"; gateMint: boolean; gateSwap: boolean; operator: Address };
1815
1913
 
1816
- /** The collector's ranges on a pad's creator choices (`PadPolicy`, decisions 056), WAD fractions: of the pot, and of every reserve per rung. */
1914
+ /** The collector's ranges on a pad's creator choices (`PadPolicy`, decisions 056, 057), WAD fractions of the pot. */
1817
1915
  export interface PadRanges {
1818
1916
  /** The protocol's share of the pot, recorded into each pad at birth. */
1819
1917
  protocolShare: bigint;
@@ -1827,21 +1925,21 @@ const MAX_U64 = (1n << 64n) - 1n;
1827
1925
  const MAX_U128 = (1n << 128n) - 1n;
1828
1926
 
1829
1927
  /**
1830
- * The salt that carries a pad's split (`PadPolicy.pack`, decisions 056): `creatorShare` in the top 64 bits, `burnPerRung` in
1831
- * the next 64 (WAD fractions), a nonce in the low 128 that tells two otherwise identical pads apart. It is part of the pool's
1928
+ * The salt that carries a pad's split (`PadPolicy.pack`, decisions 056, 057): `creatorShare` in the top 64 bits, `burnShare` in
1929
+ * the next 64 (WAD fractions of the pot), a nonce in the low 128 that tells two otherwise identical pads apart. It is part of the pool's
1832
1930
  * identity, so it is chosen at launch (`FPoolLaunchArgs.salt`) and never changes; the policy refuses a split outside its ranges.
1833
1931
  */
1834
- export function padSalt(s: { creatorShare: bigint; burnPerRung: bigint; nonce?: bigint }): Hex {
1932
+ export function padSalt(s: { creatorShare: bigint; burnShare: bigint; nonce?: bigint }): Hex {
1835
1933
  const nonce = s.nonce ?? 0n;
1836
- if (s.creatorShare < 0n || s.creatorShare > MAX_U64 || s.burnPerRung < 0n || s.burnPerRung > MAX_U64) throw new Error("logswap: a pad's split is a WAD fraction that fits 64 bits");
1934
+ if (s.creatorShare < 0n || s.creatorShare > MAX_U64 || s.burnShare < 0n || s.burnShare > MAX_U64) throw new Error("logswap: a pad's split is a WAD fraction that fits 64 bits");
1837
1935
  if (nonce < 0n || nonce > MAX_U128) throw new Error("logswap: a pad salt's nonce fits 128 bits");
1838
- return `0x${((s.creatorShare << 192n) | (s.burnPerRung << 128n) | nonce).toString(16).padStart(64, "0")}` as Hex;
1936
+ return `0x${((s.creatorShare << 192n) | (s.burnShare << 128n) | nonce).toString(16).padStart(64, "0")}` as Hex;
1839
1937
  }
1840
1938
 
1841
1939
  /** The inverse of {@link padSalt}. */
1842
- export function padSplitOfSalt(salt: Hex): { creatorShare: bigint; burnPerRung: bigint; nonce: bigint } {
1940
+ export function padSplitOfSalt(salt: Hex): { creatorShare: bigint; burnShare: bigint; nonce: bigint } {
1843
1941
  const v = BigInt(salt);
1844
- return { creatorShare: v >> 192n, burnPerRung: (v >> 128n) & MAX_U64, nonce: v & MAX_U128 };
1942
+ return { creatorShare: v >> 192n, burnShare: (v >> 128n) & MAX_U64, nonce: v & MAX_U128 };
1845
1943
  }
1846
1944
 
1847
1945
  /** The policy's terms for one pool, by its kind. */
@@ -1862,9 +1960,9 @@ export async function getFPoolRules(c: LogswapClient, s: Pick<FPoolState, "poolI
1862
1960
  rd<bigint>(padPolicyAbi, "burnMin"),
1863
1961
  rd<bigint>(padPolicyAbi, "burnMax"),
1864
1962
  ]);
1865
- const [creatorShare, burnPerRung, protocolShare] = split;
1963
+ const [creatorShare, burnShare, protocolShare] = split;
1866
1964
  const ranges: PadRanges = { protocolShare: protocolNow, creatorShareMin, creatorShareMax, burnMin, burnMax };
1867
- return { kind: "pad", gen, rung: gen, threshold, pot, creatorShare, protocolShare, burnPerRung, ranges };
1965
+ return { kind: "pad", gen, rung: gen, threshold, pot, creatorShare, protocolShare, burnShare, ranges };
1868
1966
  }
1869
1967
  case "pol": {
1870
1968
  const [keepQuote, minQuoteAtSeed] = await Promise.all([rd<bigint>(polPolicyAbi, "keepQuote"), rd<bigint>(polPolicyAbi, "minQuoteAtSeed")]);
@@ -1952,7 +2050,7 @@ export interface FPolicyTerms {
1952
2050
  carried: Address;
1953
2051
  }
1954
2052
 
1955
- /** Every F pool's state in one lens call (the raw manager struct: quote, phi, authority, kind, seeded, gen, n, Q, L, shares, theta0, bigSigma, harvestedTheta). */
2053
+ /** Every F pool's raw state in one lens call (the manager's `Pool` struct: quote, swapFee, policy, seeded, gen, n, restructureBlock, asks, cap, Q, bigSigma, L, shares, refLogFloor). */
1956
2054
  export async function getFPoolsRaw(c: LogswapClient, poolIds: Hex[]): Promise<readonly unknown[]> {
1957
2055
  const { logswapLensAbi } = await import("./generated.js");
1958
2056
  return (await c.public.readContract({ address: c.addresses.lens, abi: logswapLensAbi, functionName: "getFPools", args: [poolIds] } as never)) as readonly unknown[];