@logswap/sdk 0.5.40 → 0.5.41

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
@@ -63,28 +63,27 @@ export interface FPoolState {
63
63
  compositeX: bigint;
64
64
  /** Σ = ∫ Σ_j w_j (dx_j)², accrued even though the fee is fixed — the kernel stays calculable. */
65
65
  bigSigma: bigint;
66
- /** Creator (private) or a governance contract (public). Holds the floor lever. */
66
+ /** The cold key (decisions 037): seeds the pool, hands it over, names it, gates it (private). A creator, a treasury, a governance contract. */
67
67
  authority: Address;
68
68
  /** The proposed next authority, until it accepts — zero when nothing is pending (decisions 020). */
69
69
  pendingAuthority: Address;
70
- /** The desk's hot key (`ma-private.md` §6): may reshape composition, never move value out. Zero when none. */
70
+ /**
71
+ * The hot key that RUNS the pool (decisions 037): `harvest`, `dissolve`, `setLegs`. Zero means
72
+ * the authority itself — see {@link fPoolOperatorOf}. Appointed two-step on a private pool.
73
+ */
71
74
  operator: Address;
75
+ /** The proposed next operator, until it accepts — zero when nothing is pending. */
76
+ pendingOperator: Address;
72
77
  /** The sponsor's own label for this pool, mutable and never identity. "" when unnamed. */
73
78
  name: string;
74
- /** `harvest` may not take Q/L under this (WAD log-distance); raise-only (decisions 021). */
75
- minBuffer: bigint;
76
79
  /** Only `allowed` accounts may receive minted shares. */
77
80
  gateMint: boolean;
78
81
  /** Only `allowed` takers may swap. */
79
82
  gateSwap: boolean;
80
- /** The block of the last lever / composition action: no swap runs in it. */
83
+ /** The block of the last harvest / composition action: no swap runs in it. */
81
84
  restructureBlock: number;
82
- /**
83
- * The strike is locked (decisions 027, formerly `feesOnly`): `harvest` resets the floor to θ₀
84
- * and never past it, so only income can leave; and the pool cannot be dissolved while it bids.
85
- * The pad's promise. Pairs with a locked seed (shares at 0xdead) — the class is the two bits.
86
- */
87
- lockStrike: boolean;
85
+ /** The rung — see {@link FPoolKind}. */
86
+ kind: FPoolKind;
88
87
  /** False before the first seed — and between a dissolve and the next one (decisions 032). */
89
88
  seeded: boolean;
90
89
  /**
@@ -116,7 +115,7 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
116
115
  quote: Address;
117
116
  phi: bigint;
118
117
  authority: Address;
119
- lockStrike: boolean;
118
+ kind: number;
120
119
  seeded: boolean;
121
120
  gen: number;
122
121
  n: number;
@@ -131,7 +130,7 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
131
130
  harvestedTheta: bigint;
132
131
  pendingAuthority: Address;
133
132
  operator: Address;
134
- minBuffer: bigint;
133
+ pendingOperator: Address;
135
134
  incomeTaken: bigint;
136
135
  name: Hex;
137
136
  }>("getPool", [poolId]);
@@ -157,7 +156,7 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
157
156
  const weights = legs.map((l) => l[1]);
158
157
  const x = legs.map((l) => l[2]); // derived by the manager: x_j = ln(w_j·L / R_j) (decisions 026)
159
158
  const reserves = legs.map((l) => l[3]); // the state
160
- const { quote, Q, L, phi, theta0, bigSigma, authority, pendingAuthority, operator, minBuffer, gateMint, gateSwap, restructureBlock, lockStrike, seeded, gen } = p;
159
+ const { quote, Q, L, phi, theta0, bigSigma, authority, pendingAuthority, operator, pendingOperator, gateMint, gateSwap, restructureBlock, seeded, gen } = p;
161
160
  const totalSupply = p.shares;
162
161
 
163
162
  return {
@@ -176,12 +175,12 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
176
175
  authority,
177
176
  pendingAuthority,
178
177
  operator,
178
+ pendingOperator,
179
179
  name: bytes32ToText(p.name),
180
- minBuffer,
181
180
  gateMint,
182
181
  gateSwap,
183
182
  restructureBlock: Number(restructureBlock),
184
- lockStrike,
183
+ kind: fPoolKindOf(p.kind),
185
184
  seeded,
186
185
  gen: Number(gen),
187
186
  totalSupply,
@@ -189,7 +188,27 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
189
188
  };
190
189
  }
191
190
 
192
- /** Which of the two products a pool is configured as. A UI has to choose what to render. */
191
+ /**
192
+ * The kind of pool (decisions 036, 038). `public`: the protocol's own pool — born with the fee
193
+ * collector as its authority (which may hand it to a governance contract); no gates, no operator.
194
+ * `private`, the desk: every entry — gates, a list, an operator that reshapes the legs between
195
+ * generations, an unbounded harvest, the seed kept. `pol`, a treasury's pool or a basket: no
196
+ * gates, no operator — composition and access fixed at birth; harvest unbounded, seed the
197
+ * seeder's. `pad`, a launch: a POL whose harvest never lifts the floor above the seed floor and
198
+ * whose seed the contract minted to 0xdead — one asset, open to all. private → pol → pad is the
199
+ * ladder of rigidity; public is a pol held by the protocol.
200
+ */
201
+ export type FPoolKind = "public" | "private" | "pol" | "pad";
202
+ /** The key's enum values. */
203
+ export const F_KIND = { public: 0, private: 1, pol: 2, pad: 3 } as const;
204
+ const F_KIND_NAMES: readonly FPoolKind[] = ["public", "private", "pol", "pad"];
205
+ export function fPoolKindOf(n: number | bigint): FPoolKind {
206
+ const k = F_KIND_NAMES[Number(n)];
207
+ if (!k) throw new Error(`logswap: unknown pool kind ${n}`);
208
+ return k;
209
+ }
210
+
211
+ /** Which of the two products a pool is configured as — a pad is a launch, anything else a basket/pool. A UI has to choose what to render. */
193
212
  export type FPoolShape = "launch" | "basket";
194
213
 
195
214
  export interface FPoolDescription {
@@ -218,7 +237,7 @@ export async function describeFPool(c: LogswapClient, s: FPoolState): Promise<FP
218
237
  ? await Promise.all([rd<bigint>("feePerL"), rd<bigint>("edgePerL")])
219
238
  : [0n, 0n];
220
239
  return {
221
- shape: s.bases.length === 1 ? "launch" : "basket",
240
+ shape: s.kind === "pad" ? "launch" : "basket", // the kind, not n = 1 (decisions 036)
222
241
  legs: s.bases.length,
223
242
  atFloor: s.Q === 0n,
224
243
  feePerL,
@@ -436,8 +455,9 @@ export async function fPoolPreviewRollIn(
436
455
  * Remove quote, lifting θ — price-neutral, no base moves, and since decisions 028 a DIVIDEND to
437
456
  * every share: the amount goes into the pool's pot and each holder's slice is settled lazily on
438
457
  * their next transfer, mint or burn, or pulled with {@link fPoolClaim}. The sponsor's own slice —
439
- * its shares' and the locked seed's at 0xdead — is pushed to `to` at once. Under `lockStrike`
440
- * the lift stops at θ₀ (income only); on every live pool it stops at `LEVER_FLOOR` (Q/L ≥ ln 1.25).
458
+ * its shares' and the locked seed's at 0xdead — is pushed to `to` at once. On a pad
459
+ * the lift stops at the seed floor θ₀ (income only); nothing else bounds it (decisions 035) — an
460
+ * unlocked pool's authority may take Q to zero.
441
461
  */
442
462
  export async function fPoolHarvest(c: LogswapClient, a: { poolId: Hex; amount: bigint; to?: Address; account: Address }) {
443
463
  return writeFPool(c, a.poolId, "harvest", [a.amount, a.to ?? a.account], a.account);
@@ -464,9 +484,6 @@ export async function fPoolDividendOf(c: LogswapClient, poolId: Hex, holder: Add
464
484
  } as never) as Promise<bigint>;
465
485
  }
466
486
 
467
- /** The lever's floor: `harvest` may not take Q/L under ln 1.25 — the floor stays ≥ 20% below the mark (decisions 025). */
468
- export const LEVER_FLOOR = 223143551314209755n;
469
-
470
487
  // ─── internals ────────────────────────────────────────────────────────────────
471
488
 
472
489
  function defaultDeadline(): bigint {
@@ -779,8 +796,8 @@ export interface FPoolCreateArgs {
779
796
  weights: bigint[];
780
797
  /** Fixed fee, WAD. The base-for-base fee is pinned at 2φ on-chain. */
781
798
  phi: bigint;
782
- /** true: the strike is locked — harvest stops at θ₀ and the pool cannot be dissolved live (the pad's posture; decisions 027). */
783
- lockStrike: boolean;
799
+ /** The rung (decisions 036) — see {@link FPoolKind}. In the key: the pool's identity, never changed after. */
800
+ kind: FPoolKind;
784
801
  authority: Address;
785
802
  /**
786
803
  * The differentiator, and nothing else. Identity is the key's hash, so one sponsor cannot run
@@ -820,7 +837,7 @@ export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
820
837
  const sum = a.weights.reduce((x, y) => x + y, 0n);
821
838
  if (sum !== 10n ** 18n) throw new Error(`logswap: weights sum to ${sum}, expected 1e18`);
822
839
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
823
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, lockStrike: a.lockStrike, authority: a.authority, salt: a.salt ?? ZERO_SALT };
840
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
824
841
  const wallet = requireWallet(c);
825
842
  const { request } = await c.public.simulateContract({
826
843
  address: c.addresses.fPoolManager!,
@@ -835,7 +852,7 @@ export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
835
852
  /** The pool id is the key's hash — pure, so it can be read before or after creation. */
836
853
  export async function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): Promise<Hex> {
837
854
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
838
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, lockStrike: a.lockStrike, authority: a.authority, salt: a.salt ?? ZERO_SALT };
855
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
839
856
  return c.public.readContract({
840
857
  address: c.addresses.fPoolManager!,
841
858
  abi: fPoolManagerAbi,
@@ -872,64 +889,121 @@ export async function fPoolSeed(
872
889
  }
873
890
 
874
891
  /**
875
- * Retire the live generation (decisions 025, 032). Allowed at Q ≤ L/1e9 on every pool — the
876
- * market filled the ask — and on a pool that is not strike-locked at any Q. The live (S, Q, R_j)
877
- * become a frozen bundle every share of this generation redeems ({@link fPoolRedeem}) or rolls
878
- * ({@link fPoolRollIn}), forever; the pool is unseeded until the next {@link fPoolSeed}, which
879
- * opens generation `gen + 1` under the same id, gates, lists and promises.
892
+ * Retire the live generation (decisions 025, 032, 035) — the raw entry. Allowed only at an empty
893
+ * bid, Q ≤ L/1e9, on every pool: the market took it, or the authority harvested it out first
894
+ * ({@link fPoolRetire} does both in one transaction). The live (S, Q, R_j) become a frozen bundle
895
+ * every share of this generation redeems ({@link fPoolRedeem}) or rolls ({@link fPoolRollIn}),
896
+ * forever; the pool is unseeded until the next {@link fPoolSeed}, which opens generation
897
+ * `gen + 1` under the same id, gates and lists.
880
898
  */
881
899
  export async function fPoolDissolve(c: LogswapClient, a: { poolId: Hex; account: Address }) {
882
900
  return writeFPool(c, a.poolId, "dissolve", [], a.account);
883
901
  }
884
902
 
885
903
  /**
886
- * The sponsor's relaunch as ONE transaction (decisions 025, 032): `dissolve` the live generation,
887
- * `redeem` the sponsor's own shares of it (when `shares` > 0), `seed` the next generation with
888
- * what came back — the manager's `multicall`, the sponsor as sender throughout, the same pool.
889
- * Gates, allowlist, operator, `minBuffer` and names carry over; anything else goes in `extra`,
890
- * pre-encoded against {@link fPoolManagerAbi}. The seed names reserves (decisions 029): `r0`
891
- * outright, or `x0` marks against the key's `weights`.
904
+ * Retire the live generation as ONE transaction: the harvests that empty the bid
905
+ * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
906
+ * then `dissolve` — the manager's `multicall`, the authority as sender. What a sponsor's
907
+ * "dissolve" button should call. Throws before sending on a pad that still bids.
908
+ */
909
+ export async function fPoolRetire(c: LogswapClient, a: { poolId: Hex; account: Address }) {
910
+ const harvests = await fPoolEmptyingHarvests(c, a.poolId);
911
+ if (harvests.length === 0) return fPoolDissolve(c, a);
912
+ const enc = (functionName: string, args: unknown[]) =>
913
+ encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
914
+ const calls: Hex[] = [...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])), enc("dissolve", [a.poolId])];
915
+ return writeFPool0(c, "multicall", [calls], a.account);
916
+ }
917
+
918
+ /**
919
+ * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
920
+ * `dissolve` (decisions 035: a dissolve needs an empty bid on every pool). One `harvest(Q)` when a
921
+ * collector is set. With none, the cut on income STAYS in the pool as LP quote (decisions 002)
922
+ * and reads as taken income, so a second harvest — capital, no cut — takes the rest; the
923
+ * contract's own arithmetic, replayed. `[]` when the bid is already empty. Throws on a
924
+ * pad that still bids (its bid empties only as the market sells it down), and in
925
+ * the one shape two harvests cannot empty (no collector, income above Q — harvest by hand).
926
+ */
927
+ export async function fPoolEmptyingHarvests(c: LogswapClient, poolId: Hex): Promise<bigint[]> {
928
+ const rd = <T>(functionName: string, args: readonly unknown[] = []) =>
929
+ c.public.readContract({ address: c.addresses.fPoolManager!, abi: fPoolManagerAbi, functionName, args } as never) as Promise<T>;
930
+ const [p, feePerL, fee, exempt, collector] = await Promise.all([
931
+ rd<{ Q: bigint; L: bigint; kind: number; incomeTaken: bigint }>("getPool", [poolId]),
932
+ rd<bigint>("feePerL", [poolId]),
933
+ rd<bigint>("HARVEST_FEE"),
934
+ rd<boolean>("harvestFeeExempt", [poolId]),
935
+ rd<Address>("feeCollector"),
936
+ ]);
937
+ if (p.Q === 0n) return [];
938
+ if (p.kind === F_KIND.pad) throw new Error("logswap: a pad is retired only once its bid is empty (decisions 035)");
939
+ const earned = feePerL > 0n ? (feePerL * p.L) / WAD : 0n;
940
+ let taken = p.incomeTaken;
941
+ let q = p.Q;
942
+ const amounts: bigint[] = [];
943
+ for (let i = 0; i < 2 && q > 0n; i++) {
944
+ const amount = q;
945
+ const left = earned > taken ? earned - taken : 0n;
946
+ const taxable = amount < left ? amount : left;
947
+ taken += taxable;
948
+ const pFee = exempt ? 0n : (taxable * fee) / WAD;
949
+ const leaving = pFee !== 0n && collector !== ZERO_ADDRESS ? amount : amount - pFee;
950
+ q -= leaving;
951
+ amounts.push(amount);
952
+ }
953
+ if (q > 0n) throw new Error("logswap: the bid does not empty in two harvests (no collector, income above Q) — harvest by hand first");
954
+ return amounts;
955
+ }
956
+
957
+ /**
958
+ * The relaunch as ONE transaction (decisions 025, 032, 035, 037): `harvest` the bid out — a
959
+ * dividend to every share, {@link fPoolEmptyingHarvests} — `dissolve` the live generation at the
960
+ * empty bid, `redeem` the caller's own shares of it (when `shares` > 0), optionally `setLegs` the
961
+ * next generation's composition (`legs`: a private pool's reshape — retire, admit, reweight), and
962
+ * `seed` it with what came back — the manager's `multicall`, one sender throughout, the same
963
+ * pool. The operator's when one is appointed and the seed's inventory is its own; by default the
964
+ * authority's. Gates, allowlist and names carry over; anything else goes in `extra`, pre-encoded
965
+ * against {@link fPoolManagerAbi}. The seed names reserves (decisions 029): `r0` outright — one
966
+ * per leg of the NEXT table, 0 for a retired leg — or `x0` marks against the next table's
967
+ * `weights`.
892
968
  */
893
969
  export async function fPoolRelaunch(
894
970
  c: LogswapClient,
895
971
  a: {
896
972
  poolId: Hex;
897
- /** the sponsor's shares of the retiring generation to redeem inside the batch — its whole balance, usually; 0 to keep the claim */
973
+ /** the caller's shares of the retiring generation to redeem inside the batch — its whole balance, usually; 0 to keep the claim */
898
974
  shares: bigint;
899
975
  L0: bigint;
900
976
  Q0: bigint;
977
+ /** the next generation's composition (decisions 037): one weight per existing leg (0 retires it), then one per new base */
978
+ legs?: { weights: bigint[]; newBases?: Address[] };
901
979
  extra?: Hex[];
902
980
  account: Address;
903
981
  } & ({ r0: bigint[] } | { x0: bigint[]; weights: bigint[] }),
904
982
  ) {
905
- const gen = await fPoolGen(c, a.poolId);
906
- const r0 = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x));
983
+ const [gen, harvests] = await Promise.all([fPoolGen(c, a.poolId), fPoolEmptyingHarvests(c, a.poolId)]);
984
+ 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)));
907
985
  const enc = (functionName: string, args: unknown[]) =>
908
986
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
909
987
  const calls: Hex[] = [
988
+ ...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])),
910
989
  enc("dissolve", [a.poolId]),
911
990
  ...(a.shares > 0n ? [enc("redeem", [a.poolId, BigInt(gen), a.account, a.shares])] : []),
991
+ ...(a.legs ? [enc("setLegs", [a.poolId, a.legs.weights, a.legs.newBases ?? []])] : []),
912
992
  enc("seed", [a.poolId, a.L0, r0, a.Q0]),
913
993
  ...(a.extra ?? []),
914
994
  ];
915
995
  return writeFPool0(c, "multicall", [calls], a.account);
916
996
  }
917
997
 
918
- /** ERC-6909 shares sent here are locked for good — the seed's custody (custody.md §4(b)). */
998
+ /** Where a pad's seed lives (decisions 036): `seed` mints every share of a pad here itself. */
919
999
  export const DEAD_ADDRESS: Address = "0x000000000000000000000000000000000000dEaD";
920
- /** The manager's `MIN_SHARES`: the seed mints this much to 0xdead itself, the rest to the seeder. */
1000
+ /** The manager's `MIN_SHARES`: the seed mints this much to 0xdead on every pool, the rest to the seeder — or, on a pad, to 0xdead too. */
921
1001
  export const F_MIN_SHARES = 1000n;
922
1002
 
923
1003
  export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
924
1004
  L0: bigint;
925
1005
  /** Quote the seed pulls. A pad seeds at 0 — pure float, a resting ask, nothing raised. */
926
1006
  Q0: bigint;
927
- /**
928
- * Send every share the seed mints to the creator (`L0 − MIN_SHARES`) to 0xdead in the same
929
- * batch: the float and the raise can never be burned out from under the buyers. The lever
930
- * (harvest) is the authority's and survives it; the exit does not. A pad's default posture.
931
- */
932
- lockSeed?: boolean;
933
1007
  /**
934
1008
  * An opening buy of leg `j` (an index into `bases` AS GIVEN) for `amountIn` of quote, ON THE
935
1009
  * MANAGER — the same sender, a plain allowance to the manager, the router nowhere near it. A
@@ -940,8 +1014,6 @@ export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
940
1014
  openBuy?: { j: number; amountIn: bigint; minOut?: bigint };
941
1015
  /** The pool's label, set in the batch (≤ 31 bytes of UTF-8). */
942
1016
  name?: string;
943
- /** Gate minting, list the creator, appoint them operator (decisions 021) — three calls, in the batch. */
944
- privatePool?: boolean;
945
1017
  /**
946
1018
  * The pool already exists (a previous launch got as far as `initialize` and no further — a seed
947
1019
  * that reverted, a closed tab, before the launch was one transaction), so `initialize` is left
@@ -968,24 +1040,25 @@ export function fPoolLaunchCalls(
968
1040
  const given = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x));
969
1041
  if (given.length !== a.bases.length) throw new Error(`logswap: ${given.length} reserves for ${a.bases.length} legs`);
970
1042
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights, given);
971
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, lockStrike: a.lockStrike, authority: a.authority, salt: a.salt ?? ZERO_SALT };
1043
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
972
1044
  const enc = (functionName: string, args: unknown[]) =>
973
1045
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
974
1046
  const calls: Hex[] = [];
975
1047
  if (!a.initialized) calls.push(enc("initialize", [key]));
976
1048
  calls.push(enc("seed", [poolId, a.L0, companions[1]!, a.Q0]));
977
- // gen 0 by construction: a launch is the pool's FIRST seed — a relaunch is `fPoolRelaunch`
978
- if (a.lockSeed) calls.push(enc("transfer", [DEAD_ADDRESS, fPoolShareId(poolId, 0), a.L0 - F_MIN_SHARES]));
1049
+ // gen 0 by construction: a launch is the pool's FIRST seed — a relaunch is `fPoolRelaunch`.
1050
+ // No transfer to 0xdead: on a pad `seed` mints every share there itself (decisions 036).
979
1051
  if (a.openBuy) {
980
1052
  const j = bases.indexOf(a.bases[a.openBuy.j]!);
981
1053
  if (j < 0) throw new Error(`logswap: openBuy.j = ${a.openBuy.j} is not a leg`);
982
1054
  calls.push(enc("swapQuoteIn", [poolId, BigInt(j), a.openBuy.amountIn, a.openBuy.minOut ?? 0n, a.account]));
983
1055
  }
984
1056
  if (a.name) calls.push(enc("setName", [poolId, textToBytes32(a.name)]));
985
- if (a.privatePool) {
1057
+ if (a.kind === "private") {
1058
+ // the desk's opening posture: gated minting, the creator listed. No appointment: the
1059
+ // authority is the operator until one is appointed (decisions 037)
986
1060
  calls.push(enc("setGates", [poolId, true, false]));
987
1061
  calls.push(enc("setAllowed", [poolId, [a.account], true]));
988
- calls.push(enc("appointOperator", [poolId, a.account]));
989
1062
  }
990
1063
  calls.push(...(a.extra ?? []));
991
1064
  return calls;
@@ -1091,45 +1164,44 @@ export async function fPoolSponsorName(c: LogswapClient, who: Address): Promise<
1091
1164
  return bytes32ToText(word);
1092
1165
  }
1093
1166
 
1094
- /** Appoint (or clear, with the zero address) the operator — the hot key that reshapes and never moves value out. */
1095
- export async function fPoolAppointOperator(c: LogswapClient, a: { poolId: Hex; operator: Address; account: Address }) {
1096
- return writeFPool(c, a.poolId, "appointOperator", [a.operator], a.account);
1167
+ /** Who runs the pool (decisions 037): the appointed operator, or the authority until one is. */
1168
+ export function fPoolOperatorOf(s: Pick<FPoolState, "authority" | "operator">): Address {
1169
+ return s.operator === ZERO_ADDRESS ? s.authority : s.operator;
1097
1170
  }
1098
1171
 
1099
1172
  /**
1100
- * Raise the floor guard: `harvest` may not take Q/L under `minBuffer` (WAD log-distance; 20% below
1101
- * spot is ln 1.25 ≈ 0.223e18). Raise-only — a commitment the authority cannot walk back.
1173
+ * Propose the operator (authority; private pools only) — the hot key that runs the pool: `harvest`,
1174
+ * `dissolve`, `setLegs`. Two-step: the proposed address takes the role with
1175
+ * {@link fPoolAcceptOperator}. The zero address clears at once — the role returns to the authority.
1102
1176
  */
1103
- export async function fPoolRaiseMinBuffer(c: LogswapClient, a: { poolId: Hex; minBuffer: bigint; account: Address }) {
1104
- return writeFPool(c, a.poolId, "raiseMinBuffer", [a.minBuffer], a.account);
1177
+ export async function fPoolAppointOperator(c: LogswapClient, a: { poolId: Hex; operator: Address; account: Address }) {
1178
+ return writeFPool(c, a.poolId, "appointOperator", [a.operator], a.account);
1105
1179
  }
1106
1180
 
1107
- /**
1108
- * Set leg `j`'s liquidity (operator, sole LP only): grow, shrink, retire (`newLj = 0`), or re-admit
1109
- * a retired leg holding `r` of the base (decisions 029 — the price follows, `p = newLj / r`; use
1110
- * {@link fPoolReserveAt} from a mark). For a live leg leave `r` undefined — its price is the
1111
- * market's, the reserve scales with the liquidity. Base moves single-sidedly through the lock;
1112
- * shares adjust to hold the share value.
1113
- */
1114
- export async function fPoolSetLegL(
1115
- c: LogswapClient,
1116
- a: { poolId: Hex; j: number; newLj: bigint; r?: bigint; account: Address },
1117
- ) {
1118
- return writeFPool(c, a.poolId, "setLegL", [BigInt(a.j), a.newLj, a.r ?? 0n], a.account);
1181
+ /** The proposed operator takes the role. */
1182
+ export async function fPoolAcceptOperator(c: LogswapClient, a: { poolId: Hex; account: Address }) {
1183
+ return writeFPool(c, a.poolId, "acceptOperator", [], a.account);
1119
1184
  }
1120
1185
 
1121
- /** Admit a base the pool has never held: `R` of it against liquidity `Lj`, so at `p = Lj / R` (operator, sole LP only). */
1122
- export async function fPoolAdmitLeg(
1186
+ /**
1187
+ * The next generation's composition (operator; private pools only; ONLY while the pool is
1188
+ * unseeded — between a dissolve and the seed that prices it, decisions 037). `weights` has one
1189
+ * entry per leg the pool has ever had, in index order (0 retires a leg; indices are never reused),
1190
+ * then one per base in `newBases`, appended; they sum to 1e18 exactly. No value moves: the
1191
+ * following {@link fPoolSeed} names every reserve. A live reshape is {@link fPoolRelaunch} with
1192
+ * `legs` set.
1193
+ */
1194
+ export async function fPoolSetLegs(
1123
1195
  c: LogswapClient,
1124
- a: { poolId: Hex; base: Address; Lj: bigint; R: bigint; account: Address },
1196
+ a: { poolId: Hex; weights: bigint[]; newBases?: Address[]; account: Address },
1125
1197
  ) {
1126
- return writeFPool(c, a.poolId, "admitLeg", [a.base, a.Lj, a.R], a.account);
1198
+ return writeFPool(c, a.poolId, "setLegs", [a.weights, a.newBases ?? []], a.account);
1127
1199
  }
1128
1200
 
1129
1201
  /**
1130
1202
  * Move shares (ERC-6909 `transfer`) — to another holder, or to `0xdead` to lock them for good.
1131
1203
  * Burning to the dead address is a sponsor's way to make its liquidity permanent: the shares can
1132
- * never be redeemed, and they never count against the sole-LP precondition.
1204
+ * never be redeemed.
1133
1205
  */
1134
1206
  export async function fPoolTransferShares(
1135
1207
  c: LogswapClient,
@@ -1155,7 +1227,7 @@ export interface DiscoveredFPool {
1155
1227
  bases: Address[];
1156
1228
  weights: bigint[];
1157
1229
  phi: bigint;
1158
- lockStrike: boolean;
1230
+ kind: FPoolKind;
1159
1231
  authority: Address;
1160
1232
  /** The block the pool was initialized in — its age, for annualising realized income. */
1161
1233
  block: bigint;
@@ -1174,7 +1246,7 @@ export interface DiscoveredFPool {
1174
1246
 
1175
1247
  /**
1176
1248
  * Every F pool ever created, from `Initialize` logs alone. Possible only because the event
1177
- * carries the full key preimage (bases, weights, phi, lockStrike) — the poolId is a hash and could
1249
+ * carries the full key preimage (bases, weights, phi, pad) — the poolId is a hash and could
1178
1250
  * never be inverted; contracts PR #17 exists for exactly this call.
1179
1251
  */
1180
1252
  export async function discoverFPools(
@@ -1205,7 +1277,7 @@ export async function discoverFPools(
1205
1277
  return logs.map((l) => {
1206
1278
  const a = l.args as {
1207
1279
  poolId: Hex; quote: Address; authority: Address; bases: readonly Address[];
1208
- weights: readonly bigint[]; phi: bigint; lockStrike: boolean;
1280
+ weights: readonly bigint[]; phi: bigint; kind: number;
1209
1281
  };
1210
1282
  const q0 = q0Of.get(a.poolId.toLowerCase());
1211
1283
  return {
@@ -1214,12 +1286,12 @@ export async function discoverFPools(
1214
1286
  bases: [...a.bases],
1215
1287
  weights: [...a.weights],
1216
1288
  phi: a.phi,
1217
- lockStrike: a.lockStrike,
1289
+ kind: fPoolKindOf(a.kind),
1218
1290
  authority: a.authority,
1219
1291
  block: l.blockNumber ?? 0n,
1220
1292
  seeded: q0 !== undefined,
1221
1293
  q0: q0 ?? 0n,
1222
- shape: a.bases.length === 1 && (q0 ?? 0n) === 0n ? "launch" : "basket",
1294
+ shape: a.kind === F_KIND.pad ? "launch" : "basket", // the kind, not n = 1: a mono POL is a pool (decisions 036)
1223
1295
  };
1224
1296
  });
1225
1297
  }
@@ -1258,7 +1330,7 @@ export async function fPoolShareHolders(
1258
1330
  .sort((a, b) => (b.shares > a.shares ? 1 : -1));
1259
1331
  }
1260
1332
 
1261
- /** Every F pool's state in one lens call (the raw manager struct: quote, phi, authority, lockStrike, seeded, gen, n, Q, L, shares, theta0, bigSigma, harvestedTheta). */
1333
+ /** 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). */
1262
1334
  export async function getFPoolsRaw(c: LogswapClient, poolIds: Hex[]): Promise<readonly unknown[]> {
1263
1335
  const { logswapLensAbi } = await import("./generated.js");
1264
1336
  return (await c.public.readContract({ address: c.addresses.lens, abi: logswapLensAbi, functionName: "getFPools", args: [poolIds] } as never)) as readonly unknown[];