@logswap/sdk 0.5.51 → 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/fees.d.ts +52 -0
- package/dist/fees.js +79 -0
- package/dist/fpool.d.ts +42 -7
- package/dist/fpool.js +44 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/liquidity.d.ts +43 -15
- package/dist/liquidity.js +46 -25
- package/dist/write.js +2 -1
- package/package.json +2 -1
- package/src/fees.ts +103 -0
- package/src/fpool.ts +52 -8
- package/src/index.ts +1 -0
- package/src/liquidity.ts +82 -37
- package/src/write.ts +2 -1
package/dist/fees.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fees.ts — swap fees over a window, from `Swap` logs.
|
|
3
|
+
*
|
|
4
|
+
* `F` cannot answer "what did takers pay this week": it is per unit of L, it carries the mint-fee
|
|
5
|
+
* credits as well as swap fees, and it has no window. Each `Swap` event carries the quote leg and
|
|
6
|
+
* φ, and the fee is a fixed fraction of that leg, so the sum over a window is exact up to rounding:
|
|
7
|
+
*
|
|
8
|
+
* quote in (price up): the curve takes q/(1+φ), the fee is q·φ/(1+φ)
|
|
9
|
+
* quote out (price down): the curve releases q/(1−φ), the fee is q·φ/(1−φ)
|
|
10
|
+
*
|
|
11
|
+
* Both equal φ·L·|Δx| summed over the swap's segments — the same φ·|dx| per unit L that `F` books.
|
|
12
|
+
*/
|
|
13
|
+
import type { LogswapClient } from "./client.js";
|
|
14
|
+
import type { DiscoveredMarket } from "./pools.js";
|
|
15
|
+
/** The fee one swap paid, in quote, from its `Swap` event's fields. */
|
|
16
|
+
export declare function swapFeeOf(e: {
|
|
17
|
+
baseIn: boolean;
|
|
18
|
+
amountIn: bigint;
|
|
19
|
+
amountOut: bigint;
|
|
20
|
+
}, phi: bigint): bigint;
|
|
21
|
+
/** Swap fees paid over a window, in the market's quote units. */
|
|
22
|
+
export interface SwapFeeWindow {
|
|
23
|
+
fromBlock: bigint;
|
|
24
|
+
toBlock: bigint;
|
|
25
|
+
/** Window start, unix seconds — `since`, or the first block's timestamp, never before the market opened. */
|
|
26
|
+
from: bigint;
|
|
27
|
+
/** Window end, unix seconds — the last block's timestamp. */
|
|
28
|
+
to: bigint;
|
|
29
|
+
seconds: bigint;
|
|
30
|
+
swapFees: bigint;
|
|
31
|
+
swaps: number;
|
|
32
|
+
}
|
|
33
|
+
/** The first block with timestamp ≥ `t`, by bisection over `[lo, hi]`; `hi` when none is earlier. */
|
|
34
|
+
export declare function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi: bigint): Promise<bigint>;
|
|
35
|
+
/**
|
|
36
|
+
* Sum the swap fees of market `m` over a window: from `since` (unix seconds) or `fromBlock`, to
|
|
37
|
+
* `toBlock` (default latest). Either start is clamped to the market's opening, so a period longer
|
|
38
|
+
* than the market's life measures its whole life and `seconds` says how long that was.
|
|
39
|
+
*/
|
|
40
|
+
export declare function swapFees(c: LogswapClient, m: DiscoveredMarket, opts?: {
|
|
41
|
+
since?: bigint;
|
|
42
|
+
fromBlock?: bigint;
|
|
43
|
+
toBlock?: bigint;
|
|
44
|
+
}): Promise<SwapFeeWindow>;
|
|
45
|
+
/**
|
|
46
|
+
* The window's swap fees over the market's CURRENT active L, annualised linearly over the window.
|
|
47
|
+
* A fraction: 0.12 is 12%. Linear: fees are paid out, so none compound.
|
|
48
|
+
*
|
|
49
|
+
* Per unit of L. An uncapped position is worth L·(x − ξ + 1), so its return on capital is this
|
|
50
|
+
* divided by (x − ξ + 1).
|
|
51
|
+
*/
|
|
52
|
+
export declare function swapFeeApy(w: Pick<SwapFeeWindow, "swapFees" | "seconds">, lActive: bigint): number;
|
package/dist/fees.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fees.ts — swap fees over a window, from `Swap` logs.
|
|
3
|
+
*
|
|
4
|
+
* `F` cannot answer "what did takers pay this week": it is per unit of L, it carries the mint-fee
|
|
5
|
+
* credits as well as swap fees, and it has no window. Each `Swap` event carries the quote leg and
|
|
6
|
+
* φ, and the fee is a fixed fraction of that leg, so the sum over a window is exact up to rounding:
|
|
7
|
+
*
|
|
8
|
+
* quote in (price up): the curve takes q/(1+φ), the fee is q·φ/(1+φ)
|
|
9
|
+
* quote out (price down): the curve releases q/(1−φ), the fee is q·φ/(1−φ)
|
|
10
|
+
*
|
|
11
|
+
* Both equal φ·L·|Δx| summed over the swap's segments — the same φ·|dx| per unit L that `F` books.
|
|
12
|
+
*/
|
|
13
|
+
import { cPoolManagerAbi } from "./generated.js";
|
|
14
|
+
const WAD = 10n ** 18n;
|
|
15
|
+
const YEAR_SECONDS = 31_536_000;
|
|
16
|
+
/** The fee one swap paid, in quote, from its `Swap` event's fields. */
|
|
17
|
+
export function swapFeeOf(e, phi) {
|
|
18
|
+
return e.baseIn ? (e.amountOut * phi) / (WAD - phi) : (e.amountIn * phi) / (WAD + phi);
|
|
19
|
+
}
|
|
20
|
+
/** The first block with timestamp ≥ `t`, by bisection over `[lo, hi]`; `hi` when none is earlier. */
|
|
21
|
+
export async function blockAtOrAfter(c, t, lo, hi) {
|
|
22
|
+
while (lo < hi) {
|
|
23
|
+
const mid = (lo + hi) / 2n;
|
|
24
|
+
if ((await c.public.getBlock({ blockNumber: mid })).timestamp < t)
|
|
25
|
+
lo = mid + 1n;
|
|
26
|
+
else
|
|
27
|
+
hi = mid;
|
|
28
|
+
}
|
|
29
|
+
return lo;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sum the swap fees of market `m` over a window: from `since` (unix seconds) or `fromBlock`, to
|
|
33
|
+
* `toBlock` (default latest). Either start is clamped to the market's opening, so a period longer
|
|
34
|
+
* than the market's life measures its whole life and `seconds` says how long that was.
|
|
35
|
+
*/
|
|
36
|
+
export async function swapFees(c, m, opts = {}) {
|
|
37
|
+
const toBlock = opts.toBlock ?? (await c.public.getBlockNumber());
|
|
38
|
+
const [opened, last] = await Promise.all([
|
|
39
|
+
c.public.getBlock({ blockNumber: m.blockNumber }),
|
|
40
|
+
c.public.getBlock({ blockNumber: toBlock }),
|
|
41
|
+
]);
|
|
42
|
+
let fromBlock;
|
|
43
|
+
let from;
|
|
44
|
+
if (opts.since !== undefined && opts.since > opened.timestamp) {
|
|
45
|
+
fromBlock = await blockAtOrAfter(c, opts.since, m.blockNumber, toBlock);
|
|
46
|
+
from = opts.since;
|
|
47
|
+
}
|
|
48
|
+
else if (opts.fromBlock !== undefined && opts.fromBlock > m.blockNumber) {
|
|
49
|
+
fromBlock = opts.fromBlock;
|
|
50
|
+
from = (await c.public.getBlock({ blockNumber: fromBlock })).timestamp;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
fromBlock = m.blockNumber;
|
|
54
|
+
from = opened.timestamp;
|
|
55
|
+
}
|
|
56
|
+
const ev = cPoolManagerAbi.find((x) => x.type === "event" && x.name === "Swap");
|
|
57
|
+
if (!ev)
|
|
58
|
+
throw new Error("logswap: Swap event missing from the generated ABI");
|
|
59
|
+
const logs = fromBlock > toBlock
|
|
60
|
+
? []
|
|
61
|
+
: await c.public.getLogs({ address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock });
|
|
62
|
+
let fees = 0n;
|
|
63
|
+
for (const l of logs)
|
|
64
|
+
fees += swapFeeOf({ baseIn: l.args.baseIn, amountIn: l.args.amountIn, amountOut: l.args.amountOut }, l.args.phi);
|
|
65
|
+
const to = last.timestamp;
|
|
66
|
+
return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps: logs.length };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The window's swap fees over the market's CURRENT active L, annualised linearly over the window.
|
|
70
|
+
* A fraction: 0.12 is 12%. Linear: fees are paid out, so none compound.
|
|
71
|
+
*
|
|
72
|
+
* Per unit of L. An uncapped position is worth L·(x − ξ + 1), so its return on capital is this
|
|
73
|
+
* divided by (x − ξ + 1).
|
|
74
|
+
*/
|
|
75
|
+
export function swapFeeApy(w, lActive) {
|
|
76
|
+
if (w.seconds <= 0n || lActive <= 0n)
|
|
77
|
+
return 0;
|
|
78
|
+
return (Number(w.swapFees) / Number(lActive)) * (YEAR_SECONDS / Number(w.seconds));
|
|
79
|
+
}
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
253
|
-
|
|
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,
|
|
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
|
|
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
|
package/dist/index.d.ts
CHANGED
|
@@ -19,5 +19,6 @@ export * from "./onboard.js";
|
|
|
19
19
|
export * from "./indexer.js";
|
|
20
20
|
export * from "./tokencache.js";
|
|
21
21
|
export * from "./fpool.js";
|
|
22
|
+
export * from "./fees.js";
|
|
22
23
|
export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
|
|
23
24
|
export * from "./admin.js";
|
package/dist/index.js
CHANGED
|
@@ -19,5 +19,6 @@ export * from "./onboard.js";
|
|
|
19
19
|
export * from "./indexer.js";
|
|
20
20
|
export * from "./tokencache.js";
|
|
21
21
|
export * from "./fpool.js";
|
|
22
|
+
export * from "./fees.js";
|
|
22
23
|
export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
|
|
23
24
|
export * from "./admin.js";
|
package/dist/liquidity.d.ts
CHANGED
|
@@ -33,8 +33,14 @@ export interface MintArgs extends WriteOptions {
|
|
|
33
33
|
cap?: bigint;
|
|
34
34
|
/** Exposure to add. */
|
|
35
35
|
L: bigint;
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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.
|
|
76
|
-
minSwapOut
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
50
|
-
a.maxQuoteIn
|
|
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
|
|
75
|
-
a.maxQuoteIn
|
|
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
|
|
96
|
+
a.minSwapOut,
|
|
98
97
|
a.targetFloor,
|
|
99
98
|
a.cap ?? NO_CAP,
|
|
100
99
|
a.L,
|
|
101
|
-
a.maxIn
|
|
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
|
|
121
|
-
a.maxQuoteIn
|
|
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
|
|
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
|
|
147
|
-
a.maxQuoteIn
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
a.minQuoteOut
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"clean": "rm -rf dist src/generated.ts",
|
|
27
27
|
"test": "vitest run",
|
|
28
|
+
"test:isolated": "bash scripts/test-isolated.sh",
|
|
28
29
|
"test:watch": "vitest",
|
|
29
30
|
"prepublishOnly": "npm run codegen && npm run build"
|
|
30
31
|
},
|
package/src/fees.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fees.ts — swap fees over a window, from `Swap` logs.
|
|
3
|
+
*
|
|
4
|
+
* `F` cannot answer "what did takers pay this week": it is per unit of L, it carries the mint-fee
|
|
5
|
+
* credits as well as swap fees, and it has no window. Each `Swap` event carries the quote leg and
|
|
6
|
+
* φ, and the fee is a fixed fraction of that leg, so the sum over a window is exact up to rounding:
|
|
7
|
+
*
|
|
8
|
+
* quote in (price up): the curve takes q/(1+φ), the fee is q·φ/(1+φ)
|
|
9
|
+
* quote out (price down): the curve releases q/(1−φ), the fee is q·φ/(1−φ)
|
|
10
|
+
*
|
|
11
|
+
* Both equal φ·L·|Δx| summed over the swap's segments — the same φ·|dx| per unit L that `F` books.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { LogswapClient } from "./client.js";
|
|
15
|
+
import { cPoolManagerAbi } from "./generated.js";
|
|
16
|
+
import type { DiscoveredMarket } from "./pools.js";
|
|
17
|
+
|
|
18
|
+
const WAD = 10n ** 18n;
|
|
19
|
+
const YEAR_SECONDS = 31_536_000;
|
|
20
|
+
|
|
21
|
+
/** The fee one swap paid, in quote, from its `Swap` event's fields. */
|
|
22
|
+
export function swapFeeOf(e: { baseIn: boolean; amountIn: bigint; amountOut: bigint }, phi: bigint): bigint {
|
|
23
|
+
return e.baseIn ? (e.amountOut * phi) / (WAD - phi) : (e.amountIn * phi) / (WAD + phi);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Swap fees paid over a window, in the market's quote units. */
|
|
27
|
+
export interface SwapFeeWindow {
|
|
28
|
+
fromBlock: bigint;
|
|
29
|
+
toBlock: bigint;
|
|
30
|
+
/** Window start, unix seconds — `since`, or the first block's timestamp, never before the market opened. */
|
|
31
|
+
from: bigint;
|
|
32
|
+
/** Window end, unix seconds — the last block's timestamp. */
|
|
33
|
+
to: bigint;
|
|
34
|
+
seconds: bigint;
|
|
35
|
+
swapFees: bigint;
|
|
36
|
+
swaps: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The first block with timestamp ≥ `t`, by bisection over `[lo, hi]`; `hi` when none is earlier. */
|
|
40
|
+
export async function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi: bigint): Promise<bigint> {
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = (lo + hi) / 2n;
|
|
43
|
+
if ((await c.public.getBlock({ blockNumber: mid })).timestamp < t) lo = mid + 1n;
|
|
44
|
+
else hi = mid;
|
|
45
|
+
}
|
|
46
|
+
return lo;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sum the swap fees of market `m` over a window: from `since` (unix seconds) or `fromBlock`, to
|
|
51
|
+
* `toBlock` (default latest). Either start is clamped to the market's opening, so a period longer
|
|
52
|
+
* than the market's life measures its whole life and `seconds` says how long that was.
|
|
53
|
+
*/
|
|
54
|
+
export async function swapFees(
|
|
55
|
+
c: LogswapClient,
|
|
56
|
+
m: DiscoveredMarket,
|
|
57
|
+
opts: { since?: bigint; fromBlock?: bigint; toBlock?: bigint } = {},
|
|
58
|
+
): Promise<SwapFeeWindow> {
|
|
59
|
+
const toBlock = opts.toBlock ?? (await c.public.getBlockNumber());
|
|
60
|
+
const [opened, last] = await Promise.all([
|
|
61
|
+
c.public.getBlock({ blockNumber: m.blockNumber }),
|
|
62
|
+
c.public.getBlock({ blockNumber: toBlock }),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
let fromBlock: bigint;
|
|
66
|
+
let from: bigint;
|
|
67
|
+
if (opts.since !== undefined && opts.since > opened.timestamp) {
|
|
68
|
+
fromBlock = await blockAtOrAfter(c, opts.since, m.blockNumber, toBlock);
|
|
69
|
+
from = opts.since;
|
|
70
|
+
} else if (opts.fromBlock !== undefined && opts.fromBlock > m.blockNumber) {
|
|
71
|
+
fromBlock = opts.fromBlock;
|
|
72
|
+
from = (await c.public.getBlock({ blockNumber: fromBlock })).timestamp;
|
|
73
|
+
} else {
|
|
74
|
+
fromBlock = m.blockNumber;
|
|
75
|
+
from = opened.timestamp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Ev = Extract<(typeof cPoolManagerAbi)[number], { type: "event"; name: "Swap" }>;
|
|
79
|
+
const ev = cPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Swap");
|
|
80
|
+
if (!ev) throw new Error("logswap: Swap event missing from the generated ABI");
|
|
81
|
+
|
|
82
|
+
const logs = fromBlock > toBlock
|
|
83
|
+
? []
|
|
84
|
+
: await c.public.getLogs({ address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock });
|
|
85
|
+
|
|
86
|
+
let fees = 0n;
|
|
87
|
+
for (const l of logs) fees += swapFeeOf({ baseIn: l.args.baseIn!, amountIn: l.args.amountIn!, amountOut: l.args.amountOut! }, l.args.phi!);
|
|
88
|
+
|
|
89
|
+
const to = last.timestamp;
|
|
90
|
+
return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps: logs.length };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The window's swap fees over the market's CURRENT active L, annualised linearly over the window.
|
|
95
|
+
* A fraction: 0.12 is 12%. Linear: fees are paid out, so none compound.
|
|
96
|
+
*
|
|
97
|
+
* Per unit of L. An uncapped position is worth L·(x − ξ + 1), so its return on capital is this
|
|
98
|
+
* divided by (x − ξ + 1).
|
|
99
|
+
*/
|
|
100
|
+
export function swapFeeApy(w: Pick<SwapFeeWindow, "swapFees" | "seconds">, lActive: bigint): number {
|
|
101
|
+
if (w.seconds <= 0n || lActive <= 0n) return 0;
|
|
102
|
+
return (Number(w.swapFees) / Number(lActive)) * (YEAR_SECONDS / Number(w.seconds));
|
|
103
|
+
}
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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/index.ts
CHANGED
|
@@ -19,5 +19,6 @@ export * from "./onboard.js";
|
|
|
19
19
|
export * from "./indexer.js";
|
|
20
20
|
export * from "./tokencache.js";
|
|
21
21
|
export * from "./fpool.js";
|
|
22
|
+
export * from "./fees.js";
|
|
22
23
|
export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
|
|
23
24
|
export * from "./admin.js";
|
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
|
-
|
|
62
|
-
|
|
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
|
|
86
|
-
a.maxQuoteIn
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
124
|
-
a.maxQuoteIn
|
|
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.
|
|
137
|
-
minSwapOut
|
|
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
|
-
|
|
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
|
|
168
|
+
a.minSwapOut,
|
|
161
169
|
a.targetFloor,
|
|
162
170
|
a.cap ?? NO_CAP,
|
|
163
171
|
a.L,
|
|
164
|
-
a.maxIn
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
199
|
-
a.maxQuoteIn
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
236
|
-
a.maxQuoteIn
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
|
428
|
-
a.minQuoteOut
|
|
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
|
-
|
|
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
|
/**
|