@logswap/sdk 0.5.52 → 0.5.53

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/dist/fpool.d.ts CHANGED
@@ -807,7 +807,12 @@ export interface FPoolSwapArgs {
807
807
  /** Leg index into `bases`. */
808
808
  j: number;
809
809
  amountIn: bigint;
810
- minOut?: bigint;
810
+ /**
811
+ * The least the swap must return. Required: an F swap is exact-in, and without a floor on the
812
+ * output a front-run takes the difference. {@link fPoolQuoteSwap} is exact; bound it with
813
+ * {@link withSlippage} (`"output"`).
814
+ */
815
+ minOut: bigint;
811
816
  /** Who receives the out leg. Defaults to the account. */
812
817
  to?: Address;
813
818
  /** Absolute unix seconds. Defaulted to a bounded window, as every router write is. */
@@ -825,10 +830,12 @@ export declare function fPoolSwapBaseForBase(c: LogswapClient, a: FPoolSwapArgs
825
830
  k: number;
826
831
  }): Promise<`0x${string}`>;
827
832
  /** Deposit dL/L of every reserve, receive shares ∝ dL. θ is untouched by construction. */
828
- export declare function fPoolMint(c: LogswapClient, a: {
833
+ export declare function fPoolMint(c: LogswapClient,
834
+ /** `maxQuoteIn` is required: the quote leg is Q·dL/L at landing, and Q moves with every swap. */
835
+ a: {
829
836
  poolId: Hex;
830
837
  dL: bigint;
831
- maxQuoteIn?: bigint;
838
+ maxQuoteIn: bigint;
832
839
  account: Address;
833
840
  to?: Address;
834
841
  }): Promise<`0x${string}`>;
@@ -837,13 +844,28 @@ export declare function fPoolMint(c: LogswapClient, a: {
837
844
  * By owner since contracts `c782811` (decisions 019): the account burns its own shares; an
838
845
  * operator may pass another owner's address, as on the C manager's `burnById`.
839
846
  */
840
- export declare function fPoolBurn(c: LogswapClient, a: {
847
+ export declare function fPoolBurn(c: LogswapClient,
848
+ /** `minQuoteOut` is required: the quote slice is Q·shares/S at landing. See {@link fPoolPreviewBurn}. */
849
+ a: {
841
850
  poolId: Hex;
842
851
  shares: bigint;
843
- minQuoteOut?: bigint;
852
+ minQuoteOut: bigint;
844
853
  account: Address;
845
854
  owner?: Address;
846
855
  }): Promise<`0x${string}`>;
856
+ /**
857
+ * What burning `shares` returns at today's state — the manager's own arithmetic: every reserve and
858
+ * the quote pro rata, rounded down (FPoolManager.burn). Exact until the next swap or mint.
859
+ */
860
+ export declare function fPoolPreviewBurn(st: Pick<FPoolState, "Q" | "reserves" | "totalSupply">, shares: bigint): {
861
+ quoteOut: bigint;
862
+ basesOut: bigint[];
863
+ };
864
+ /**
865
+ * The quote {@link fPoolMint} pulls for `dL` at today's state: Q·dL/L, rounded up (the pool never
866
+ * under-collects), before any mint fee — so bound it with {@link withSlippage} (`"input"`).
867
+ */
868
+ export declare function fPoolPreviewMintQuote(st: Pick<FPoolState, "Q" | "L">, dL: bigint): bigint;
847
869
  /**
848
870
  * Quote needed to zap into exposure `dL`. An UPPER bound: buying each leg raises its mark, so the
849
871
  * mint consumes less base than was bought for it and every zap ends with a refund. Size from this
@@ -874,15 +896,28 @@ export declare function fPoolZapIn(c: LogswapClient, a: {
874
896
  * cross the floor; only the burn's own quote slice touches the quote leg. Into the quote every
875
897
  * base leg is sold, which is the one path a drained quote leg can block.
876
898
  */
877
- export declare function fPoolZapOut(c: LogswapClient, a: {
899
+ export declare function fPoolZapOut(c: LogswapClient,
900
+ /** `minOut` is required: the sale routes every leg through the curve. See {@link fPoolPreviewZapOut}. */
901
+ a: {
878
902
  poolId: Hex;
879
903
  shares: bigint;
880
904
  tokenOut: Address;
881
- minOut?: bigint;
905
+ minOut: bigint;
882
906
  to?: Address;
883
907
  account: Address;
884
908
  deadline?: bigint;
885
909
  }): Promise<`0x${string}`>;
910
+ /**
911
+ * What {@link fPoolZapOut} returns at today's state: the router's own `zapOut`, simulated as the
912
+ * holder with no bound. Exact, but it needs what the real call needs — the shares, and the router
913
+ * as the holder's ERC-6909 operator — so it throws where the zap-out itself would revert.
914
+ */
915
+ export declare function fPoolPreviewZapOut(c: LogswapClient, a: {
916
+ poolId: Hex;
917
+ shares: bigint;
918
+ tokenOut: Address;
919
+ account: Address;
920
+ }): Promise<bigint>;
886
921
  /**
887
922
  * Roll a retired generation's bundle into the pool's live one (decisions 025, 032), on the
888
923
  * router under one lock: redeem `shares` of generation `gen` in place — the legs, the quote and
package/dist/fpool.js CHANGED
@@ -222,7 +222,7 @@ export function fPoolShareValue(s) {
222
222
  async function fSwap(c, a, kind, k = 0) {
223
223
  const st = await getFPool(c, a.poolId);
224
224
  const pull = kind === FPoolQuoteKind.QuoteIn ? st.quote : st.bases[a.j];
225
- return writeRouter(c, "swapExactIn", [a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut ?? 0n, a.to ?? a.account, a.deadline ?? defaultDeadline()], a.account, [pull]);
225
+ return writeRouter(c, "swapExactIn", [a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut, a.to ?? a.account, a.deadline ?? defaultDeadline()], a.account, [pull]);
226
226
  }
227
227
  export async function fPoolSwapQuoteIn(c, a) {
228
228
  return fSwap(c, a, FPoolQuoteKind.QuoteIn);
@@ -240,17 +240,37 @@ export async function fPoolSwapBaseForBase(c, a) {
240
240
  }
241
241
  // ─── liquidity ────────────────────────────────────────────────────────────────
242
242
  /** Deposit dL/L of every reserve, receive shares ∝ dL. θ is untouched by construction. */
243
- export async function fPoolMint(c, a) {
243
+ export async function fPoolMint(c,
244
+ /** `maxQuoteIn` is required: the quote leg is Q·dL/L at landing, and Q moves with every swap. */
245
+ a) {
244
246
  // shares go to `to` (the account by default); on a gated pool `to` must be allowed
245
- return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn ?? 2n ** 256n - 1n], a.account);
247
+ return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn], a.account);
246
248
  }
247
249
  /**
248
250
  * Burn shares for the pro-rata slice of every reserve. Available at any Q, and after dissolution.
249
251
  * By owner since contracts `c782811` (decisions 019): the account burns its own shares; an
250
252
  * operator may pass another owner's address, as on the C manager's `burnById`.
251
253
  */
252
- export async function fPoolBurn(c, a) {
253
- return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut ?? 0n], a.account);
254
+ export async function fPoolBurn(c,
255
+ /** `minQuoteOut` is required: the quote slice is Q·shares/S at landing. See {@link fPoolPreviewBurn}. */
256
+ a) {
257
+ return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut], a.account);
258
+ }
259
+ /**
260
+ * What burning `shares` returns at today's state — the manager's own arithmetic: every reserve and
261
+ * the quote pro rata, rounded down (FPoolManager.burn). Exact until the next swap or mint.
262
+ */
263
+ export function fPoolPreviewBurn(st, shares) {
264
+ if (st.totalSupply === 0n)
265
+ return { quoteOut: 0n, basesOut: st.reserves.map(() => 0n) };
266
+ return { quoteOut: (st.Q * shares) / st.totalSupply, basesOut: st.reserves.map((r) => (r * shares) / st.totalSupply) };
267
+ }
268
+ /**
269
+ * The quote {@link fPoolMint} pulls for `dL` at today's state: Q·dL/L, rounded up (the pool never
270
+ * under-collects), before any mint fee — so bound it with {@link withSlippage} (`"input"`).
271
+ */
272
+ export function fPoolPreviewMintQuote(st, dL) {
273
+ return st.L === 0n ? 0n : (st.Q * dL + st.L - 1n) / st.L;
254
274
  }
255
275
  // ─── zaps (on the router — one router, one Permit2 spender) ───────────────────
256
276
  /**
@@ -288,9 +308,26 @@ export async function fPoolZapIn(c, a) {
288
308
  * cross the floor; only the burn's own quote slice touches the quote leg. Into the quote every
289
309
  * base leg is sold, which is the one path a drained quote leg can block.
290
310
  */
291
- export async function fPoolZapOut(c, a) {
311
+ export async function fPoolZapOut(c,
312
+ /** `minOut` is required: the sale routes every leg through the curve. See {@link fPoolPreviewZapOut}. */
313
+ a) {
292
314
  const to = a.to ?? a.account;
293
- return writeRouter(c, "zapOut", [a.poolId, a.shares, a.tokenOut, a.minOut ?? 0n, to, a.deadline ?? defaultDeadline()], a.account);
315
+ return writeRouter(c, "zapOut", [a.poolId, a.shares, a.tokenOut, a.minOut, to, a.deadline ?? defaultDeadline()], a.account);
316
+ }
317
+ /**
318
+ * What {@link fPoolZapOut} returns at today's state: the router's own `zapOut`, simulated as the
319
+ * holder with no bound. Exact, but it needs what the real call needs — the shares, and the router
320
+ * as the holder's ERC-6909 operator — so it throws where the zap-out itself would revert.
321
+ */
322
+ export async function fPoolPreviewZapOut(c, a) {
323
+ const { result } = await c.public.simulateContract({
324
+ address: c.addresses.router,
325
+ abi: logswapRouterAbi,
326
+ functionName: "zapOut",
327
+ args: [a.poolId, a.shares, a.tokenOut, 0n, a.account, defaultDeadline()],
328
+ account: a.account,
329
+ });
330
+ return result;
294
331
  }
295
332
  /**
296
333
  * Roll a retired generation's bundle into the pool's live one (decisions 025, 032), on the
@@ -33,8 +33,14 @@ export interface MintArgs extends WriteOptions {
33
33
  cap?: bigint;
34
34
  /** Exposure to add. */
35
35
  L: bigint;
36
- maxBaseIn?: bigint;
37
- maxQuoteIn?: bigint;
36
+ /**
37
+ * The most the mint may pull, per token. REQUIRED: the pulled amounts depend on spot when the
38
+ * transaction lands, so an unbounded mint can be sandwiched into depositing far more than the
39
+ * preview showed. Derive them from the preview with {@link withSlippage} (`"input"`); pass
40
+ * {@link UNBOUNDED} only where the price cannot move under you.
41
+ */
42
+ maxBaseIn: bigint;
43
+ maxQuoteIn: bigint;
38
44
  }
39
45
  /**
40
46
  * Mint at `targetFloor`, blending across the two adjacent rungs when it is off-grid.
@@ -55,8 +61,9 @@ export interface CreateMarketArgs extends WriteOptions {
55
61
  targetFloor: bigint;
56
62
  /** The founding exposure; must exceed the manager's `minBackstopL` lock. */
57
63
  L: bigint;
58
- maxBaseIn?: bigint;
59
- maxQuoteIn?: bigint;
64
+ /** Required, as on {@link mint}. Creation is atomic at `x0`, so the flows are fixed by the arguments; {@link UNBOUNDED} is safe here only for that reason. */
65
+ maxBaseIn: bigint;
66
+ maxQuoteIn: bigint;
60
67
  }
61
68
  /**
62
69
  * Create a market ATOMICALLY: `initialize` at `x0` plus the backstop-establishing first mint in
@@ -72,12 +79,13 @@ export interface ZapArgs extends WriteOptions {
72
79
  /** Fund entirely from base (true) or entirely from quote (false). */
73
80
  fundWithBase: boolean;
74
81
  amountIn: bigint;
75
- /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. 0 = unbounded. */
76
- minSwapOut?: bigint;
82
+ /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. Required. */
83
+ minSwapOut: bigint;
77
84
  targetFloor: bigint;
78
85
  cap?: bigint;
79
86
  L: bigint;
80
- maxIn?: bigint;
87
+ /** The most of the funding token the zap may pull. Required. */
88
+ maxIn: bigint;
81
89
  }
82
90
  /**
83
91
  * Fund a mint from a single asset: the router swaps the imbalanced part through the curve, then
@@ -96,8 +104,9 @@ export interface UpdateArgs extends WriteOptions {
96
104
  toCap?: bigint;
97
105
  /** New exposure. `0n` exits the position entirely. */
98
106
  newL: bigint;
99
- maxBaseIn?: bigint;
100
- maxQuoteIn?: bigint;
107
+ /** Required: see {@link MintArgs}. {@link previewEdit} gives the exact flows to bound. */
108
+ maxBaseIn: bigint;
109
+ maxQuoteIn: bigint;
101
110
  }
102
111
  /** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
103
112
  export declare function update(c: LogswapClient, a: UpdateArgs): Promise<Hash>;
@@ -107,16 +116,16 @@ export interface MoveArgs extends WriteOptions {
107
116
  shares: bigint;
108
117
  toFloor: bigint;
109
118
  toCap?: bigint;
110
- maxBaseIn?: bigint;
111
- maxQuoteIn?: bigint;
119
+ /** Required: see {@link MintArgs}. */
120
+ maxBaseIn: bigint;
121
+ maxQuoteIn: bigint;
112
122
  }
113
123
  /**
114
124
  * Reposition at constant exposure — the L-pinned parent of `floor`, `cap` and `shift`; in kind.
115
125
  *
116
126
  * **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
117
127
  * priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
118
- * The defaults below are unbounded for callers who genuinely want that; pass `maxBaseIn` /
119
- * `maxQuoteIn` from a preview for anything user-facing.
128
+ * The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
120
129
  */
121
130
  export declare function move(c: LogswapClient, a: MoveArgs): Promise<Hash>;
122
131
  /** Which token the edit settles in. `kind` moves both legs; `base` / `quote` nets the other side through the curve. */
@@ -128,6 +137,13 @@ export interface EditArgs extends WriteOptions, PairBounds {
128
137
  shares: bigint;
129
138
  /** Default `kind`. */
130
139
  settleIn?: SettleIn;
140
+ /**
141
+ * Tolerance in basis points, applied to the flows the edit is expected to move (the lens's
142
+ * exact preview, plus a quote of the netting swap when `settleIn` is one token). Required: an
143
+ * edit's flows depend on spot when it lands. An explicit bound in `PairBounds` overrides the one
144
+ * derived for that token.
145
+ */
146
+ slippageBps: number;
131
147
  }
132
148
  export interface EditPreview {
133
149
  /** Signed, + = the caller deposits; the mint fee is inside `dQuote`. */
@@ -146,11 +162,22 @@ export declare function previewEdit(c: LogswapClient, a: {
146
162
  toCap?: bigint;
147
163
  newL: bigint;
148
164
  }): Promise<EditPreview>;
165
+ /**
166
+ * Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
167
+ * tolerance: a payment is capped from above, a receipt floored from below, and a token that does
168
+ * not move in that direction is bound to zero.
169
+ */
170
+ export declare function boundsFromFlows(dBase: bigint, dQuote: bigint, bps: number): Required<PairBounds>;
149
171
  /**
150
172
  * The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
151
173
  * nets the other. The other side's flow is read off the preview: released → sold with an exact-in
152
174
  * of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
153
175
  * other token to zero both ways, so a stale preview fails loudly rather than moving it.
176
+ *
177
+ * **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
178
+ * turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
179
+ * on today's curve, before the update changes the active liquidity — close for a small edit, and
180
+ * for a large one a bound that reverts rather than one that lets a sandwich through.
154
181
  */
155
182
  export declare function edit(c: LogswapClient, a: EditArgs & {
156
183
  toFloor: bigint;
@@ -213,8 +240,9 @@ export interface BurnArgs extends WriteOptions {
213
240
  key: PoolKey;
214
241
  ids: readonly bigint[];
215
242
  shares: readonly bigint[];
216
- minBaseOut?: bigint;
217
- minQuoteOut?: bigint;
243
+ /** The least of each token the burn must return. Required: a burn's legs depend on spot. */
244
+ minBaseOut: bigint;
245
+ minQuoteOut: bigint;
218
246
  }
219
247
  /**
220
248
  * Exit, in kind, across one or more classes. Accrued fees are swept into the same settlement.
package/dist/liquidity.js CHANGED
@@ -25,7 +25,8 @@ import { logswapLensAbi, logswapRouterAbi } from "./generated.js";
25
25
  import { NO_CAP } from "./ids.js";
26
26
  import { describeId, getPositionClass } from "./positions.js";
27
27
  import { execute, OPEN_DELTA, settleInKind, settleOnly, swapExactInAction, swapExactOutAction, updateAction, } from "./actions.js";
28
- import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, sendRouterWriteWithPermits, } from "./write.js";
28
+ import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, sendRouterWriteWithPermits, withSlippage, } from "./write.js";
29
+ import { quoteExactIn, quoteExactOut } from "./swap.js";
29
30
  /**
30
31
  * Mint at `targetFloor`, blending across the two adjacent rungs when it is off-grid.
31
32
  *
@@ -37,7 +38,6 @@ import { requireWallet, resolveDeadline, resolveRecipient, sendRouterWrite, send
37
38
  * costs zero quote too — that is what makes single-asset entry possible in both directions.
38
39
  */
39
40
  export async function mint(c, a) {
40
- const MAX = (1n << 256n) - 1n;
41
41
  return sendRouterWriteWithPermits(c, {
42
42
  abi: logswapRouterAbi,
43
43
  functionName: "mintBlended",
@@ -46,8 +46,8 @@ export async function mint(c, a) {
46
46
  a.targetFloor,
47
47
  a.cap ?? NO_CAP,
48
48
  a.L,
49
- a.maxBaseIn ?? MAX,
50
- a.maxQuoteIn ?? MAX,
49
+ a.maxBaseIn,
50
+ a.maxQuoteIn,
51
51
  resolveRecipient(c, a),
52
52
  resolveDeadline(a),
53
53
  ],
@@ -62,7 +62,6 @@ export async function mint(c, a) {
62
62
  * uncapped.
63
63
  */
64
64
  export async function createMarket(c, a) {
65
- const MAX = (1n << 256n) - 1n;
66
65
  return sendRouterWriteWithPermits(c, {
67
66
  abi: logswapRouterAbi,
68
67
  functionName: "createMarket",
@@ -71,8 +70,8 @@ export async function createMarket(c, a) {
71
70
  a.x0,
72
71
  a.targetFloor,
73
72
  a.L,
74
- a.maxBaseIn ?? MAX,
75
- a.maxQuoteIn ?? MAX,
73
+ a.maxBaseIn,
74
+ a.maxQuoteIn,
76
75
  resolveRecipient(c, a),
77
76
  resolveDeadline(a),
78
77
  ],
@@ -94,11 +93,11 @@ export async function zapIn(c, a) {
94
93
  a.key,
95
94
  a.fundWithBase,
96
95
  a.amountIn,
97
- a.minSwapOut ?? 0n,
96
+ a.minSwapOut,
98
97
  a.targetFloor,
99
98
  a.cap ?? NO_CAP,
100
99
  a.L,
101
- a.maxIn ?? (1n << 256n) - 1n,
100
+ a.maxIn,
102
101
  resolveRecipient(c, a),
103
102
  resolveDeadline(a),
104
103
  ],
@@ -106,7 +105,6 @@ export async function zapIn(c, a) {
106
105
  }
107
106
  /** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
108
107
  export async function update(c, a) {
109
- const MAX = (1n << 256n) - 1n;
110
108
  return sendRouterWriteWithPermits(c, {
111
109
  abi: logswapRouterAbi,
112
110
  functionName: "update",
@@ -117,8 +115,8 @@ export async function update(c, a) {
117
115
  a.toFloor,
118
116
  a.toCap ?? NO_CAP,
119
117
  a.newL,
120
- a.maxBaseIn ?? MAX,
121
- a.maxQuoteIn ?? MAX,
118
+ a.maxBaseIn,
119
+ a.maxQuoteIn,
122
120
  resolveRecipient(c, a),
123
121
  resolveDeadline(a),
124
122
  ],
@@ -129,11 +127,9 @@ export async function update(c, a) {
129
127
  *
130
128
  * **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
131
129
  * priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
132
- * The defaults below are unbounded for callers who genuinely want that; pass `maxBaseIn` /
133
- * `maxQuoteIn` from a preview for anything user-facing.
130
+ * The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
134
131
  */
135
132
  export async function move(c, a) {
136
- const MAX = (1n << 256n) - 1n;
137
133
  return sendRouterWrite(c, {
138
134
  abi: logswapRouterAbi,
139
135
  functionName: "move",
@@ -143,8 +139,8 @@ export async function move(c, a) {
143
139
  a.shares,
144
140
  a.toFloor,
145
141
  a.toCap ?? NO_CAP,
146
- a.maxBaseIn ?? MAX,
147
- a.maxQuoteIn ?? MAX,
142
+ a.maxBaseIn,
143
+ a.maxQuoteIn,
148
144
  resolveRecipient(c, a),
149
145
  resolveDeadline(a),
150
146
  ],
@@ -161,11 +157,26 @@ export async function previewEdit(c, a) {
161
157
  }));
162
158
  return { dBase: r[0], dQuote: r[1], fees: r[2] };
163
159
  }
160
+ /**
161
+ * Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
162
+ * tolerance: a payment is capped from above, a receipt floored from below, and a token that does
163
+ * not move in that direction is bound to zero.
164
+ */
165
+ export function boundsFromFlows(dBase, dQuote, bps) {
166
+ const inb = (d) => (d > 0n ? withSlippage(d, bps, "input") : 0n);
167
+ const outb = (d) => (d < 0n ? withSlippage(-d, bps, "output") : 0n);
168
+ return { maxBaseIn: inb(dBase), maxQuoteIn: inb(dQuote), minBaseOut: outb(dBase), minQuoteOut: outb(dQuote) };
169
+ }
164
170
  /**
165
171
  * The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
166
172
  * nets the other. The other side's flow is read off the preview: released → sold with an exact-in
167
173
  * of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
168
174
  * other token to zero both ways, so a stale preview fails loudly rather than moving it.
175
+ *
176
+ * **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
177
+ * turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
178
+ * on today's curve, before the update changes the active liquidity — close for a small edit, and
179
+ * for a large one a bound that reverts rather than one that lets a sandwich through.
169
180
  */
170
181
  export async function edit(c, a) {
171
182
  const { key } = a;
@@ -174,27 +185,37 @@ export async function edit(c, a) {
174
185
  updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
175
186
  ];
176
187
  const settleIn = a.settleIn ?? "kind";
188
+ const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
177
189
  let settle;
178
190
  if (settleIn === "kind") {
179
- settle = settleInKind(key, recipient, a);
191
+ settle = settleInKind(key, recipient, { ...boundsFromFlows(p.dBase, p.dQuote, a.slippageBps), ...defined(a) });
180
192
  }
181
193
  else {
182
- const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
183
194
  // the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
184
195
  // so quote's open delta after the update is dQuote itself — `fees` is reported for display only
185
196
  const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
186
197
  const otherIsBase = settleIn === "quote";
187
- if (otherFlow < 0n)
198
+ let own = settleIn === "base" ? p.dBase : p.dQuote;
199
+ if (otherFlow < 0n) {
188
200
  actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
189
- else if (otherFlow > 0n)
201
+ own -= await quoteExactIn(c, key, otherIsBase, -otherFlow);
202
+ }
203
+ else if (otherFlow > 0n) {
190
204
  actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
205
+ own += await quoteExactOut(c, key, otherIsBase, otherFlow);
206
+ }
207
+ const b = settleIn === "base" ? boundsFromFlows(own, 0n, a.slippageBps) : boundsFromFlows(0n, own, a.slippageBps);
191
208
  settle = settleOnly(key, recipient, settleIn, {
192
- maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
193
- minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
209
+ maxIn: settleIn === "base" ? (a.maxBaseIn ?? b.maxBaseIn) : (a.maxQuoteIn ?? b.maxQuoteIn),
210
+ minOut: settleIn === "base" ? (a.minBaseOut ?? b.minBaseOut) : (a.minQuoteOut ?? b.minQuoteOut),
194
211
  });
195
212
  }
196
213
  return execute(c, { actions, settle, deadline: a.deadline, recipient });
197
214
  }
215
+ /** The explicit bounds a caller passed, without the undefined ones (so they do not erase derived ones). */
216
+ function defined(b) {
217
+ return Object.fromEntries(["maxBaseIn", "maxQuoteIn", "minBaseOut", "minQuoteOut"].filter((k) => b[k] !== undefined).map((k) => [k, b[k]]));
218
+ }
198
219
  /** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
199
220
  async function classOf(c, key, id, shares) {
200
221
  const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
@@ -297,8 +318,8 @@ export async function burn(c, a) {
297
318
  a.key,
298
319
  a.ids,
299
320
  a.shares,
300
- a.minBaseOut ?? 0n,
301
- a.minQuoteOut ?? 0n,
321
+ a.minBaseOut,
322
+ a.minQuoteOut,
302
323
  resolveRecipient(c, a),
303
324
  resolveDeadline(a),
304
325
  ],
package/dist/write.js CHANGED
@@ -53,7 +53,8 @@ export function withSlippage(quoted, bps, side) {
53
53
  if (bps < 0 || bps > 10_000)
54
54
  throw new Error(`logswap: slippage bps out of range: ${bps}`);
55
55
  const b = BigInt(Math.round(bps));
56
- return side === "output" ? (quoted * (10000n - b)) / 10000n : (quoted * (10000n + b)) / 10000n;
56
+ // an input bound rounds UP: flooring it would put a wei-exact quote at bps = 0 one wei short
57
+ return side === "output" ? (quoted * (10000n - b)) / 10000n : (quoted * (10000n + b) + 9999n) / 10000n;
57
58
  }
58
59
  /**
59
60
  * Send a router write with a gas buffer over an explicit estimate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logswap/sdk",
3
- "version": "0.5.52",
3
+ "version": "0.5.53",
4
4
  "description": "TypeScript SDK for Logswap, the liquidity primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager / FPoolManager / Router / Lens surface.",
5
5
  "license": "GPL-2.0-or-later",
6
6
  "type": "module",
package/src/fpool.ts CHANGED
@@ -359,7 +359,12 @@ export interface FPoolSwapArgs {
359
359
  /** Leg index into `bases`. */
360
360
  j: number;
361
361
  amountIn: bigint;
362
- minOut?: bigint;
362
+ /**
363
+ * The least the swap must return. Required: an F swap is exact-in, and without a floor on the
364
+ * output a front-run takes the difference. {@link fPoolQuoteSwap} is exact; bound it with
365
+ * {@link withSlippage} (`"output"`).
366
+ */
367
+ minOut: bigint;
363
368
  /** Who receives the out leg. Defaults to the account. */
364
369
  to?: Address;
365
370
  /** Absolute unix seconds. Defaulted to a bounded window, as every router write is. */
@@ -379,7 +384,7 @@ async function fSwap(c: LogswapClient, a: FPoolSwapArgs, kind: FPoolQuoteKind, k
379
384
  return writeRouter(
380
385
  c,
381
386
  "swapExactIn",
382
- [a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut ?? 0n, a.to ?? a.account, a.deadline ?? defaultDeadline()],
387
+ [a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut, a.to ?? a.account, a.deadline ?? defaultDeadline()],
383
388
  a.account,
384
389
  [pull],
385
390
  );
@@ -407,10 +412,11 @@ export async function fPoolSwapBaseForBase(c: LogswapClient, a: FPoolSwapArgs &
407
412
  /** Deposit dL/L of every reserve, receive shares ∝ dL. θ is untouched by construction. */
408
413
  export async function fPoolMint(
409
414
  c: LogswapClient,
410
- a: { poolId: Hex; dL: bigint; maxQuoteIn?: bigint; account: Address; to?: Address },
415
+ /** `maxQuoteIn` is required: the quote leg is Q·dL/L at landing, and Q moves with every swap. */
416
+ a: { poolId: Hex; dL: bigint; maxQuoteIn: bigint; account: Address; to?: Address },
411
417
  ) {
412
418
  // shares go to `to` (the account by default); on a gated pool `to` must be allowed
413
- return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn ?? 2n ** 256n - 1n], a.account);
419
+ return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn], a.account);
414
420
  }
415
421
 
416
422
  /**
@@ -420,9 +426,27 @@ export async function fPoolMint(
420
426
  */
421
427
  export async function fPoolBurn(
422
428
  c: LogswapClient,
423
- a: { poolId: Hex; shares: bigint; minQuoteOut?: bigint; account: Address; owner?: Address },
429
+ /** `minQuoteOut` is required: the quote slice is Q·shares/S at landing. See {@link fPoolPreviewBurn}. */
430
+ a: { poolId: Hex; shares: bigint; minQuoteOut: bigint; account: Address; owner?: Address },
424
431
  ) {
425
- return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut ?? 0n], a.account);
432
+ return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut], a.account);
433
+ }
434
+
435
+ /**
436
+ * What burning `shares` returns at today's state — the manager's own arithmetic: every reserve and
437
+ * the quote pro rata, rounded down (FPoolManager.burn). Exact until the next swap or mint.
438
+ */
439
+ export function fPoolPreviewBurn(st: Pick<FPoolState, "Q" | "reserves" | "totalSupply">, shares: bigint): { quoteOut: bigint; basesOut: bigint[] } {
440
+ if (st.totalSupply === 0n) return { quoteOut: 0n, basesOut: st.reserves.map(() => 0n) };
441
+ return { quoteOut: (st.Q * shares) / st.totalSupply, basesOut: st.reserves.map((r) => (r * shares) / st.totalSupply) };
442
+ }
443
+
444
+ /**
445
+ * The quote {@link fPoolMint} pulls for `dL` at today's state: Q·dL/L, rounded up (the pool never
446
+ * under-collects), before any mint fee — so bound it with {@link withSlippage} (`"input"`).
447
+ */
448
+ export function fPoolPreviewMintQuote(st: Pick<FPoolState, "Q" | "L">, dL: bigint): bigint {
449
+ return st.L === 0n ? 0n : (st.Q * dL + st.L - 1n) / st.L;
426
450
  }
427
451
 
428
452
  // ─── zaps (on the router — one router, one Permit2 spender) ───────────────────
@@ -475,17 +499,37 @@ export async function fPoolZapIn(
475
499
  */
476
500
  export async function fPoolZapOut(
477
501
  c: LogswapClient,
478
- a: { poolId: Hex; shares: bigint; tokenOut: Address; minOut?: bigint; to?: Address; account: Address; deadline?: bigint },
502
+ /** `minOut` is required: the sale routes every leg through the curve. See {@link fPoolPreviewZapOut}. */
503
+ a: { poolId: Hex; shares: bigint; tokenOut: Address; minOut: bigint; to?: Address; account: Address; deadline?: bigint },
479
504
  ) {
480
505
  const to = a.to ?? a.account;
481
506
  return writeRouter(
482
507
  c,
483
508
  "zapOut",
484
- [a.poolId, a.shares, a.tokenOut, a.minOut ?? 0n, to, a.deadline ?? defaultDeadline()],
509
+ [a.poolId, a.shares, a.tokenOut, a.minOut, to, a.deadline ?? defaultDeadline()],
485
510
  a.account,
486
511
  );
487
512
  }
488
513
 
514
+ /**
515
+ * What {@link fPoolZapOut} returns at today's state: the router's own `zapOut`, simulated as the
516
+ * holder with no bound. Exact, but it needs what the real call needs — the shares, and the router
517
+ * as the holder's ERC-6909 operator — so it throws where the zap-out itself would revert.
518
+ */
519
+ export async function fPoolPreviewZapOut(
520
+ c: LogswapClient,
521
+ a: { poolId: Hex; shares: bigint; tokenOut: Address; account: Address },
522
+ ): Promise<bigint> {
523
+ const { result } = await c.public.simulateContract({
524
+ address: c.addresses.router,
525
+ abi: logswapRouterAbi,
526
+ functionName: "zapOut",
527
+ args: [a.poolId, a.shares, a.tokenOut, 0n, a.account, defaultDeadline()],
528
+ account: a.account,
529
+ } as never);
530
+ return result as bigint;
531
+ }
532
+
489
533
  /**
490
534
  * Roll a retired generation's bundle into the pool's live one (decisions 025, 032), on the
491
535
  * router under one lock: redeem `shares` of generation `gen` in place — the legs, the quote and
package/src/liquidity.ts CHANGED
@@ -46,8 +46,10 @@ import {
46
46
  resolveRecipient,
47
47
  sendRouterWrite,
48
48
  sendRouterWriteWithPermits,
49
+ withSlippage,
49
50
  type WriteOptions,
50
51
  } from "./write.js";
52
+ import { quoteExactIn, quoteExactOut } from "./swap.js";
51
53
 
52
54
  /* mint ====================================================================== */
53
55
 
@@ -58,8 +60,14 @@ export interface MintArgs extends WriteOptions {
58
60
  cap?: bigint;
59
61
  /** Exposure to add. */
60
62
  L: bigint;
61
- maxBaseIn?: bigint;
62
- maxQuoteIn?: bigint;
63
+ /**
64
+ * The most the mint may pull, per token. REQUIRED: the pulled amounts depend on spot when the
65
+ * transaction lands, so an unbounded mint can be sandwiched into depositing far more than the
66
+ * preview showed. Derive them from the preview with {@link withSlippage} (`"input"`); pass
67
+ * {@link UNBOUNDED} only where the price cannot move under you.
68
+ */
69
+ maxBaseIn: bigint;
70
+ maxQuoteIn: bigint;
63
71
  }
64
72
 
65
73
  /**
@@ -73,7 +81,6 @@ export interface MintArgs extends WriteOptions {
73
81
  * costs zero quote too — that is what makes single-asset entry possible in both directions.
74
82
  */
75
83
  export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
76
- const MAX = (1n << 256n) - 1n;
77
84
  return sendRouterWriteWithPermits(c, {
78
85
  abi: logswapRouterAbi,
79
86
  functionName: "mintBlended",
@@ -82,8 +89,8 @@ export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
82
89
  a.targetFloor,
83
90
  a.cap ?? NO_CAP,
84
91
  a.L,
85
- a.maxBaseIn ?? MAX,
86
- a.maxQuoteIn ?? MAX,
92
+ a.maxBaseIn,
93
+ a.maxQuoteIn,
87
94
  resolveRecipient(c, a),
88
95
  resolveDeadline(a),
89
96
  ],
@@ -98,8 +105,9 @@ export interface CreateMarketArgs extends WriteOptions {
98
105
  targetFloor: bigint;
99
106
  /** The founding exposure; must exceed the manager's `minBackstopL` lock. */
100
107
  L: bigint;
101
- maxBaseIn?: bigint;
102
- maxQuoteIn?: bigint;
108
+ /** Required, as on {@link mint}. Creation is atomic at `x0`, so the flows are fixed by the arguments; {@link UNBOUNDED} is safe here only for that reason. */
109
+ maxBaseIn: bigint;
110
+ maxQuoteIn: bigint;
103
111
  }
104
112
 
105
113
  /**
@@ -111,7 +119,6 @@ export interface CreateMarketArgs extends WriteOptions {
111
119
  * uncapped.
112
120
  */
113
121
  export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promise<Hash> {
114
- const MAX = (1n << 256n) - 1n;
115
122
  return sendRouterWriteWithPermits(c, {
116
123
  abi: logswapRouterAbi,
117
124
  functionName: "createMarket",
@@ -120,8 +127,8 @@ export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promi
120
127
  a.x0,
121
128
  a.targetFloor,
122
129
  a.L,
123
- a.maxBaseIn ?? MAX,
124
- a.maxQuoteIn ?? MAX,
130
+ a.maxBaseIn,
131
+ a.maxQuoteIn,
125
132
  resolveRecipient(c, a),
126
133
  resolveDeadline(a),
127
134
  ],
@@ -133,12 +140,13 @@ export interface ZapArgs extends WriteOptions {
133
140
  /** Fund entirely from base (true) or entirely from quote (false). */
134
141
  fundWithBase: boolean;
135
142
  amountIn: bigint;
136
- /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. 0 = unbounded. */
137
- minSwapOut?: bigint;
143
+ /** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. Required. */
144
+ minSwapOut: bigint;
138
145
  targetFloor: bigint;
139
146
  cap?: bigint;
140
147
  L: bigint;
141
- maxIn?: bigint;
148
+ /** The most of the funding token the zap may pull. Required. */
149
+ maxIn: bigint;
142
150
  }
143
151
 
144
152
  /**
@@ -157,11 +165,11 @@ export async function zapIn(c: LogswapClient, a: ZapArgs): Promise<Hash> {
157
165
  a.key,
158
166
  a.fundWithBase,
159
167
  a.amountIn,
160
- a.minSwapOut ?? 0n,
168
+ a.minSwapOut,
161
169
  a.targetFloor,
162
170
  a.cap ?? NO_CAP,
163
171
  a.L,
164
- a.maxIn ?? (1n << 256n) - 1n,
172
+ a.maxIn,
165
173
  resolveRecipient(c, a),
166
174
  resolveDeadline(a),
167
175
  ],
@@ -178,13 +186,13 @@ export interface UpdateArgs extends WriteOptions {
178
186
  toCap?: bigint;
179
187
  /** New exposure. `0n` exits the position entirely. */
180
188
  newL: bigint;
181
- maxBaseIn?: bigint;
182
- maxQuoteIn?: bigint;
189
+ /** Required: see {@link MintArgs}. {@link previewEdit} gives the exact flows to bound. */
190
+ maxBaseIn: bigint;
191
+ maxQuoteIn: bigint;
183
192
  }
184
193
 
185
194
  /** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
186
195
  export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
187
- const MAX = (1n << 256n) - 1n;
188
196
  return sendRouterWriteWithPermits(c, {
189
197
  abi: logswapRouterAbi,
190
198
  functionName: "update",
@@ -195,8 +203,8 @@ export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
195
203
  a.toFloor,
196
204
  a.toCap ?? NO_CAP,
197
205
  a.newL,
198
- a.maxBaseIn ?? MAX,
199
- a.maxQuoteIn ?? MAX,
206
+ a.maxBaseIn,
207
+ a.maxQuoteIn,
200
208
  resolveRecipient(c, a),
201
209
  resolveDeadline(a),
202
210
  ],
@@ -209,8 +217,9 @@ export interface MoveArgs extends WriteOptions {
209
217
  shares: bigint;
210
218
  toFloor: bigint;
211
219
  toCap?: bigint;
212
- maxBaseIn?: bigint;
213
- maxQuoteIn?: bigint;
220
+ /** Required: see {@link MintArgs}. */
221
+ maxBaseIn: bigint;
222
+ maxQuoteIn: bigint;
214
223
  }
215
224
 
216
225
  /**
@@ -218,11 +227,9 @@ export interface MoveArgs extends WriteOptions {
218
227
  *
219
228
  * **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
220
229
  * priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
221
- * The defaults below are unbounded for callers who genuinely want that; pass `maxBaseIn` /
222
- * `maxQuoteIn` from a preview for anything user-facing.
230
+ * The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
223
231
  */
224
232
  export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
225
- const MAX = (1n << 256n) - 1n;
226
233
  return sendRouterWrite(c, {
227
234
  abi: logswapRouterAbi,
228
235
  functionName: "move",
@@ -232,8 +239,8 @@ export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
232
239
  a.shares,
233
240
  a.toFloor,
234
241
  a.toCap ?? NO_CAP,
235
- a.maxBaseIn ?? MAX,
236
- a.maxQuoteIn ?? MAX,
242
+ a.maxBaseIn,
243
+ a.maxQuoteIn,
237
244
  resolveRecipient(c, a),
238
245
  resolveDeadline(a),
239
246
  ],
@@ -252,6 +259,13 @@ export interface EditArgs extends WriteOptions, PairBounds {
252
259
  shares: bigint;
253
260
  /** Default `kind`. */
254
261
  settleIn?: SettleIn;
262
+ /**
263
+ * Tolerance in basis points, applied to the flows the edit is expected to move (the lens's
264
+ * exact preview, plus a quote of the netting swap when `settleIn` is one token). Required: an
265
+ * edit's flows depend on spot when it lands. An explicit bound in `PairBounds` overrides the one
266
+ * derived for that token.
267
+ */
268
+ slippageBps: number;
255
269
  }
256
270
 
257
271
  export interface EditPreview {
@@ -277,11 +291,27 @@ export async function previewEdit(
277
291
  return { dBase: r[0], dQuote: r[1], fees: r[2] };
278
292
  }
279
293
 
294
+ /**
295
+ * Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
296
+ * tolerance: a payment is capped from above, a receipt floored from below, and a token that does
297
+ * not move in that direction is bound to zero.
298
+ */
299
+ export function boundsFromFlows(dBase: bigint, dQuote: bigint, bps: number): Required<PairBounds> {
300
+ const inb = (d: bigint) => (d > 0n ? withSlippage(d, bps, "input") : 0n);
301
+ const outb = (d: bigint) => (d < 0n ? withSlippage(-d, bps, "output") : 0n);
302
+ return { maxBaseIn: inb(dBase), maxQuoteIn: inb(dQuote), minBaseOut: outb(dBase), minQuoteOut: outb(dQuote) };
303
+ }
304
+
280
305
  /**
281
306
  * The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
282
307
  * nets the other. The other side's flow is read off the preview: released → sold with an exact-in
283
308
  * of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
284
309
  * other token to zero both ways, so a stale preview fails loudly rather than moving it.
310
+ *
311
+ * **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
312
+ * turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
313
+ * on today's curve, before the update changes the active liquidity — close for a small edit, and
314
+ * for a large one a bound that reverts rather than one that lets a sandwich through.
285
315
  */
286
316
  export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; toCap?: bigint; newL: bigint }): Promise<Hash> {
287
317
  const { key } = a;
@@ -290,25 +320,39 @@ export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; to
290
320
  updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
291
321
  ];
292
322
  const settleIn = a.settleIn ?? "kind";
323
+ const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
293
324
  let settle: Settle;
294
325
  if (settleIn === "kind") {
295
- settle = settleInKind(key, recipient, a);
326
+ settle = settleInKind(key, recipient, { ...boundsFromFlows(p.dBase, p.dQuote, a.slippageBps), ...defined(a) });
296
327
  } else {
297
- const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
298
328
  // the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
299
329
  // so quote's open delta after the update is dQuote itself — `fees` is reported for display only
300
330
  const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
301
331
  const otherIsBase = settleIn === "quote";
302
- if (otherFlow < 0n) actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
303
- else if (otherFlow > 0n) actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
332
+ let own = settleIn === "base" ? p.dBase : p.dQuote;
333
+ if (otherFlow < 0n) {
334
+ actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
335
+ own -= await quoteExactIn(c, key, otherIsBase, -otherFlow);
336
+ } else if (otherFlow > 0n) {
337
+ actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
338
+ own += await quoteExactOut(c, key, otherIsBase, otherFlow);
339
+ }
340
+ const b = settleIn === "base" ? boundsFromFlows(own, 0n, a.slippageBps) : boundsFromFlows(0n, own, a.slippageBps);
304
341
  settle = settleOnly(key, recipient, settleIn, {
305
- maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
306
- minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
342
+ maxIn: settleIn === "base" ? (a.maxBaseIn ?? b.maxBaseIn) : (a.maxQuoteIn ?? b.maxQuoteIn),
343
+ minOut: settleIn === "base" ? (a.minBaseOut ?? b.minBaseOut) : (a.minQuoteOut ?? b.minQuoteOut),
307
344
  });
308
345
  }
309
346
  return execute(c, { actions, settle, deadline: a.deadline, recipient });
310
347
  }
311
348
 
349
+ /** The explicit bounds a caller passed, without the undefined ones (so they do not erase derived ones). */
350
+ function defined(b: PairBounds): PairBounds {
351
+ return Object.fromEntries(
352
+ (["maxBaseIn", "maxQuoteIn", "minBaseOut", "minQuoteOut"] as const).filter((k) => b[k] !== undefined).map((k) => [k, b[k]]),
353
+ );
354
+ }
355
+
312
356
  /** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
313
357
  async function classOf(c: LogswapClient, key: PoolKey, id: bigint, shares: bigint) {
314
358
  const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
@@ -405,8 +449,9 @@ export interface BurnArgs extends WriteOptions {
405
449
  key: PoolKey;
406
450
  ids: readonly bigint[];
407
451
  shares: readonly bigint[];
408
- minBaseOut?: bigint;
409
- minQuoteOut?: bigint;
452
+ /** The least of each token the burn must return. Required: a burn's legs depend on spot. */
453
+ minBaseOut: bigint;
454
+ minQuoteOut: bigint;
410
455
  }
411
456
 
412
457
  /**
@@ -424,8 +469,8 @@ export async function burn(c: LogswapClient, a: BurnArgs): Promise<Hash> {
424
469
  a.key,
425
470
  a.ids,
426
471
  a.shares,
427
- a.minBaseOut ?? 0n,
428
- a.minQuoteOut ?? 0n,
472
+ a.minBaseOut,
473
+ a.minQuoteOut,
429
474
  resolveRecipient(c, a),
430
475
  resolveDeadline(a),
431
476
  ],
package/src/write.ts CHANGED
@@ -68,7 +68,8 @@ export function resolveRecipient(c: LogswapClient, o?: WriteOptions): Address {
68
68
  export function withSlippage(quoted: bigint, bps: number, side: "output" | "input"): bigint {
69
69
  if (bps < 0 || bps > 10_000) throw new Error(`logswap: slippage bps out of range: ${bps}`);
70
70
  const b = BigInt(Math.round(bps));
71
- return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b)) / 10_000n;
71
+ // an input bound rounds UP: flooring it would put a wei-exact quote at bps = 0 one wei short
72
+ return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b) + 9_999n) / 10_000n;
72
73
  }
73
74
 
74
75
  /**