@exponent-labs/market-three-math 0.9.12 → 0.9.13
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/build/addLiquidity.d.ts +5 -1
- package/build/addLiquidity.js +84 -26
- package/build/addLiquidity.js.map +1 -1
- package/build/swapV2.js +143 -62
- package/build/swapV2.js.map +1 -1
- package/package.json +2 -2
- package/src/addLiquidity.ts +109 -31
- package/src/swapV2.ts +192 -110
package/src/addLiquidity.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* CLMM Add Liquidity simulation
|
|
3
3
|
* Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
|
|
4
4
|
*/
|
|
5
|
+
import { Ticks } from "@exponent-labs/exponent-fetcher"
|
|
6
|
+
|
|
5
7
|
import { simulateSwap, simulateSwapExactOut } from "./swapV2"
|
|
6
8
|
import {
|
|
7
9
|
AddLiquidityArgs,
|
|
@@ -12,15 +14,22 @@ import {
|
|
|
12
14
|
MarketThreeState,
|
|
13
15
|
SwapDirection,
|
|
14
16
|
} from "./types"
|
|
15
|
-
import { EffSnap, normalizedTimeRemaining, TicksWrapper } from "./utilsV2"
|
|
16
17
|
import { findTickByKey, getSuccessorTickKey } from "./utils"
|
|
17
|
-
import {
|
|
18
|
+
import { EffSnap, TicksWrapper, normalizedTimeRemaining } from "./utilsV2"
|
|
18
19
|
|
|
19
20
|
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
20
21
|
// Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
|
|
21
22
|
// quotes match the on-chain classic deposit path as closely as possible.
|
|
22
|
-
const GAP_TOKEN_NEEDS =
|
|
23
|
-
const
|
|
23
|
+
const GAP_TOKEN_NEEDS = 20
|
|
24
|
+
const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100
|
|
25
|
+
const SWAP_EXACT_OUT_SY_HEADROOM = 100
|
|
26
|
+
const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000
|
|
27
|
+
const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000
|
|
28
|
+
const FLASH_SY_POSITION_HEADROOM = 1_000
|
|
29
|
+
const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000
|
|
30
|
+
const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0
|
|
31
|
+
const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000
|
|
32
|
+
const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0
|
|
24
33
|
|
|
25
34
|
const EMPTY_CROSSING_TICK_STATE: CrossingTickState = {
|
|
26
35
|
principalPt: 0,
|
|
@@ -416,12 +425,12 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
416
425
|
return predecessor ?? candidateKey
|
|
417
426
|
}
|
|
418
427
|
|
|
419
|
-
const visitIntervals = (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
rightPrice: number
|
|
424
|
-
|
|
428
|
+
const visitIntervals = (
|
|
429
|
+
startKey: number,
|
|
430
|
+
priceStart: number,
|
|
431
|
+
priceEnd: number,
|
|
432
|
+
visitor: (params: { leftKey: number; rightKey: number; leftPrice: number; rightPrice: number }) => void,
|
|
433
|
+
) => {
|
|
425
434
|
if (!(priceStart < priceEnd)) {
|
|
426
435
|
return
|
|
427
436
|
}
|
|
@@ -953,8 +962,7 @@ export function simulateWrapperProvideLiquidity(
|
|
|
953
962
|
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
954
963
|
|
|
955
964
|
// Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
|
|
956
|
-
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } =
|
|
957
|
-
getCrossingTickStateFromTicks(ticks)
|
|
965
|
+
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks)
|
|
958
966
|
|
|
959
967
|
// Step 4: Calculate mock token needs using compute_token_needs_with_crossing
|
|
960
968
|
// max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
|
|
@@ -997,6 +1005,10 @@ export function simulateWrapperProvideLiquidity(
|
|
|
997
1005
|
syExchangeRate,
|
|
998
1006
|
})
|
|
999
1007
|
|
|
1008
|
+
if (!hasLpSupplyCapacity(marketState, depositResult.deltaL)) {
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1000
1012
|
return {
|
|
1001
1013
|
lpOut: depositResult.deltaL,
|
|
1002
1014
|
ytOut: Math.floor(ytOut),
|
|
@@ -1047,9 +1059,9 @@ export function simulateSwapAndSupply(
|
|
|
1047
1059
|
// Wrapper provide-liquidity-base debits base as:
|
|
1048
1060
|
// base_needed = ceil(total_sy_spent * sy_exchange_rate)
|
|
1049
1061
|
// So the strict SY budget for a user-provided base input is floor(base / rate).
|
|
1050
|
-
const syBudget = convertBaseToSyBudget(amountBase)
|
|
1062
|
+
const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate)
|
|
1051
1063
|
|
|
1052
|
-
if (syBudget <= 0) {
|
|
1064
|
+
if (syBudget <= 0 || syBudget < MIN_SWAP_AND_SUPPLY_SY_BUDGET) {
|
|
1053
1065
|
return {
|
|
1054
1066
|
lpOut: 0,
|
|
1055
1067
|
ptToBuy: 0,
|
|
@@ -1062,6 +1074,11 @@ export function simulateSwapAndSupply(
|
|
|
1062
1074
|
}
|
|
1063
1075
|
}
|
|
1064
1076
|
|
|
1077
|
+
const flashSyAvailable = Math.max(0, Number(marketState.financials.syBalance) - FLASH_SY_POSITION_HEADROOM)
|
|
1078
|
+
if (syBudget > flashSyAvailable) {
|
|
1079
|
+
return null
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1065
1082
|
const MOCK_AMOUNT = 1e9
|
|
1066
1083
|
const mockNeeds = simulateAddLiquidity(marketState, {
|
|
1067
1084
|
lowerTick,
|
|
@@ -1070,8 +1087,6 @@ export function simulateSwapAndSupply(
|
|
|
1070
1087
|
maxPt: MOCK_AMOUNT,
|
|
1071
1088
|
syExchangeRate,
|
|
1072
1089
|
})
|
|
1073
|
-
const searchBudget = Math.max(0, syBudget - SWAP_EXACT_OUT_SY_HEADROOM)
|
|
1074
|
-
|
|
1075
1090
|
// Wrapper flow still executes a flash swap even when external_pt_to_buy is 0
|
|
1076
1091
|
// (effective exact-out target becomes 2 due +2 safety margin).
|
|
1077
1092
|
let externalPtToBuy = 0
|
|
@@ -1103,6 +1118,15 @@ export function simulateSwapAndSupply(
|
|
|
1103
1118
|
externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut))
|
|
1104
1119
|
}
|
|
1105
1120
|
}
|
|
1121
|
+
const baseSwapExactOutSyHeadroom =
|
|
1122
|
+
externalPtToBuy >= SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD
|
|
1123
|
+
? SWAP_EXACT_OUT_SY_HEADROOM
|
|
1124
|
+
: MIN_SWAP_EXACT_OUT_SY_HEADROOM
|
|
1125
|
+
const swapExactOutSyHeadroom = syBudget >= LARGE_SWAP_AND_SUPPLY_SY_BUDGET ? 0 : baseSwapExactOutSyHeadroom
|
|
1126
|
+
const simulatedOverspendTolerance =
|
|
1127
|
+
syBudget >= LARGE_SWAP_AND_SUPPLY_SY_BUDGET ? MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS : 0
|
|
1128
|
+
const searchBudget = Math.max(0, syBudget - swapExactOutSyHeadroom + simulatedOverspendTolerance)
|
|
1129
|
+
const exactOutEstimateSlack = mockNeeds.sySpent === 0 ? PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK : 0
|
|
1106
1130
|
|
|
1107
1131
|
const candidateCache = new Map<number, SwapAndSupplyCandidate | null>()
|
|
1108
1132
|
const evaluateCandidate = (ptConstraint: number) => {
|
|
@@ -1118,13 +1142,35 @@ export function simulateSwapAndSupply(
|
|
|
1118
1142
|
syBudget,
|
|
1119
1143
|
key,
|
|
1120
1144
|
syExchangeRate,
|
|
1145
|
+
exactOutEstimateSlack,
|
|
1121
1146
|
)
|
|
1122
1147
|
candidateCache.set(key, candidate)
|
|
1123
1148
|
return candidate
|
|
1124
1149
|
}
|
|
1150
|
+
|
|
1151
|
+
if (mockNeeds.ptSpent <= 0) {
|
|
1152
|
+
const selected = evaluateCandidate(0)
|
|
1153
|
+
if (!selected || !hasLpSupplyCapacity(marketState, selected.lpOut)) {
|
|
1154
|
+
return null
|
|
1155
|
+
}
|
|
1156
|
+
if (!isSyDebitWithinTolerance(syBudget, selected.totalSySpent, simulatedOverspendTolerance)) {
|
|
1157
|
+
return null
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return {
|
|
1161
|
+
lpOut: selected.lpOut,
|
|
1162
|
+
ptToBuy: 0,
|
|
1163
|
+
syConstraint: syBudget,
|
|
1164
|
+
syForSwap: selected.tradeSySpent,
|
|
1165
|
+
syRemainder: selected.syRemainderAfterSwap,
|
|
1166
|
+
ptFromSwap: selected.tradePtOut,
|
|
1167
|
+
syDeposited: selected.depositSySpent,
|
|
1168
|
+
ptDeposited: selected.depositPtSpent,
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1125
1172
|
const ptCap = 1_000_000_000
|
|
1126
1173
|
let bestUnderBudget: SwapAndSupplyCandidate | null = null
|
|
1127
|
-
let leastOverspend: SwapAndSupplyCandidate | null = null
|
|
1128
1174
|
const considerCandidate = (candidate: SwapAndSupplyCandidate | null) => {
|
|
1129
1175
|
if (!candidate) {
|
|
1130
1176
|
return
|
|
@@ -1139,14 +1185,6 @@ export function simulateSwapAndSupply(
|
|
|
1139
1185
|
}
|
|
1140
1186
|
return
|
|
1141
1187
|
}
|
|
1142
|
-
|
|
1143
|
-
if (
|
|
1144
|
-
!leastOverspend ||
|
|
1145
|
-
candidate.totalSySpent < leastOverspend.totalSySpent ||
|
|
1146
|
-
(candidate.totalSySpent === leastOverspend.totalSySpent && candidate.lpOut > leastOverspend.lpOut)
|
|
1147
|
-
) {
|
|
1148
|
-
leastOverspend = candidate
|
|
1149
|
-
}
|
|
1150
1188
|
}
|
|
1151
1189
|
|
|
1152
1190
|
// Always evaluate from zero to establish a valid lower bound.
|
|
@@ -1200,14 +1238,21 @@ export function simulateSwapAndSupply(
|
|
|
1200
1238
|
considerCandidate(evaluateCandidate(pt))
|
|
1201
1239
|
}
|
|
1202
1240
|
|
|
1203
|
-
const selected = bestUnderBudget
|
|
1241
|
+
const selected = bestUnderBudget
|
|
1204
1242
|
if (!selected) {
|
|
1205
|
-
throw new Error("Unable to simulate swap & supply
|
|
1243
|
+
throw new Error("Unable to simulate swap & supply within SY budget")
|
|
1244
|
+
}
|
|
1245
|
+
if (!hasLpSupplyCapacity(marketState, selected.lpOut)) {
|
|
1246
|
+
return null
|
|
1247
|
+
}
|
|
1248
|
+
if (!isSyDebitWithinTolerance(syBudget, selected.totalSySpent, simulatedOverspendTolerance)) {
|
|
1249
|
+
return null
|
|
1206
1250
|
}
|
|
1207
1251
|
if (process.env.DEBUG_SWAP_SUPPLY === "1") {
|
|
1208
1252
|
console.log("[simulateSwapAndSupply]", {
|
|
1209
1253
|
syBudget,
|
|
1210
1254
|
searchBudget,
|
|
1255
|
+
swapExactOutSyHeadroom,
|
|
1211
1256
|
mockNeeds,
|
|
1212
1257
|
externalPtToBuy,
|
|
1213
1258
|
selected,
|
|
@@ -1248,6 +1293,7 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1248
1293
|
syBudget: number,
|
|
1249
1294
|
ptConstraint: number,
|
|
1250
1295
|
syExchangeRate: number,
|
|
1296
|
+
exactOutEstimateSlack: number,
|
|
1251
1297
|
): SwapAndSupplyCandidate | null {
|
|
1252
1298
|
let swapResult: {
|
|
1253
1299
|
sySpent: number
|
|
@@ -1261,6 +1307,7 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1261
1307
|
syBudget,
|
|
1262
1308
|
ptConstraint,
|
|
1263
1309
|
syExchangeRate,
|
|
1310
|
+
exactOutEstimateSlack,
|
|
1264
1311
|
)
|
|
1265
1312
|
} catch {
|
|
1266
1313
|
// Budget insufficient for this PT constraint
|
|
@@ -1268,7 +1315,6 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1268
1315
|
}
|
|
1269
1316
|
|
|
1270
1317
|
const depositState = swapResult.postMarketState ?? marketState
|
|
1271
|
-
const syAvailableForDeposit = Math.max(0, syBudget - swapResult.sySpent)
|
|
1272
1318
|
const depositResult = simulateAddLiquidity(depositState, {
|
|
1273
1319
|
lowerTick,
|
|
1274
1320
|
upperTick,
|
|
@@ -1302,9 +1348,39 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1302
1348
|
}
|
|
1303
1349
|
}
|
|
1304
1350
|
|
|
1305
|
-
function convertBaseToSyBudget(amountBase: number): number {
|
|
1351
|
+
function convertBaseToSyBudget(amountBase: number, syExchangeRate: number): number {
|
|
1306
1352
|
const baseAmount = Math.max(0, Math.floor(amountBase))
|
|
1307
|
-
|
|
1353
|
+
if (syExchangeRate <= 0) {
|
|
1354
|
+
return 0
|
|
1355
|
+
}
|
|
1356
|
+
return Math.floor(baseAmount / syExchangeRate)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function hasLpSupplyCapacity(marketState: MarketThreeState, deltaL: number): boolean {
|
|
1360
|
+
const deltaLiquidity = Math.ceil(deltaL)
|
|
1361
|
+
if (!Number.isFinite(deltaLiquidity) || deltaLiquidity < 0 || !Number.isSafeInteger(deltaLiquidity)) {
|
|
1362
|
+
return false
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const liquidityBalance = BigInt(marketState.financials.liquidityBalance.toString())
|
|
1366
|
+
const maxLpSupply = BigInt(marketState.configurationOptions.maxLpSupply.toString())
|
|
1367
|
+
|
|
1368
|
+
return liquidityBalance + BigInt(deltaLiquidity) <= maxLpSupply
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function isSyDebitWithinTolerance(
|
|
1372
|
+
syBudget: number,
|
|
1373
|
+
totalSySpent: number,
|
|
1374
|
+
simulatedOverspendTolerance: number,
|
|
1375
|
+
): boolean {
|
|
1376
|
+
const targetSyDebit = Math.max(0, Math.floor(syBudget))
|
|
1377
|
+
const estimatedSyDebit = Math.max(0, Math.floor(totalSySpent))
|
|
1378
|
+
const underSpendLamports = targetSyDebit - estimatedSyDebit
|
|
1379
|
+
if (underSpendLamports < 0) {
|
|
1380
|
+
return Math.abs(underSpendLamports) <= simulatedOverspendTolerance
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return underSpendLamports <= MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS
|
|
1308
1384
|
}
|
|
1309
1385
|
|
|
1310
1386
|
function simulateBuyPtExactOutWrapper(
|
|
@@ -1312,12 +1388,14 @@ function simulateBuyPtExactOutWrapper(
|
|
|
1312
1388
|
syBudget: number,
|
|
1313
1389
|
ptConstraint: number,
|
|
1314
1390
|
syExchangeRate: number,
|
|
1391
|
+
exactOutEstimateSlack: number,
|
|
1315
1392
|
): {
|
|
1316
1393
|
sySpent: number
|
|
1317
1394
|
ptOut: number
|
|
1318
1395
|
postMarketState?: MarketThreeState
|
|
1319
1396
|
} {
|
|
1320
1397
|
const maxSyBudget = Math.max(0, Math.floor(syBudget))
|
|
1398
|
+
const exactOutSyConstraint = maxSyBudget + Math.max(0, Math.floor(exactOutEstimateSlack))
|
|
1321
1399
|
const effectivePtOutTarget = Math.max(0, Math.floor(ptConstraint)) + 2
|
|
1322
1400
|
|
|
1323
1401
|
if (maxSyBudget === 0) {
|
|
@@ -1334,7 +1412,7 @@ function simulateBuyPtExactOutWrapper(
|
|
|
1334
1412
|
amountOut: targetPtOut,
|
|
1335
1413
|
syExchangeRate,
|
|
1336
1414
|
isCurrentFlashSwap: true,
|
|
1337
|
-
amountInConstraint:
|
|
1415
|
+
amountInConstraint: exactOutSyConstraint,
|
|
1338
1416
|
})
|
|
1339
1417
|
}
|
|
1340
1418
|
|
package/src/swapV2.ts
CHANGED
|
@@ -3,23 +3,19 @@
|
|
|
3
3
|
* Closely mirrors the Rust on-chain implementation from swap.rs
|
|
4
4
|
* Uses TicksWrapper to simulate RB-tree behavior
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
SwapArgs,
|
|
9
|
-
SwapDirection,
|
|
10
|
-
SwapExactOutArgs,
|
|
11
|
-
SwapOutcomeV2,
|
|
12
|
-
} from "./types"
|
|
13
|
-
import {
|
|
14
|
-
EffSnap,
|
|
15
|
-
TicksWrapper,
|
|
16
|
-
calculateFeeRate,
|
|
17
|
-
getFeeFromAmount,
|
|
18
|
-
normalizedTimeRemaining,
|
|
19
|
-
} from "./utilsV2"
|
|
6
|
+
import { MarketThreeState, SwapArgs, SwapDirection, SwapExactOutArgs, SwapOutcomeV2 } from "./types"
|
|
7
|
+
import { EffSnap, TicksWrapper, calculateFeeRate, getFeeFromAmount, normalizedTimeRemaining } from "./utilsV2"
|
|
20
8
|
|
|
21
9
|
const BASE_POINTS = 10000
|
|
22
10
|
|
|
11
|
+
function computeFlashFeeOut(ytValue: number, grossOut: number, lpFeeRate: number): number {
|
|
12
|
+
if (grossOut === 0) {
|
|
13
|
+
return 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Math.min(grossOut, Math.max(1, getFeeFromAmount(ytValue, lpFeeRate)))
|
|
17
|
+
}
|
|
18
|
+
|
|
23
19
|
/**
|
|
24
20
|
* Simulate a swap on the CLMM market (V2 - mirrors Rust closely)
|
|
25
21
|
* This is a pure function that does not mutate the market state
|
|
@@ -364,7 +360,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
364
360
|
if (args.isCurrentFlashSwap) {
|
|
365
361
|
const syInBase = syInSegmentU64 * args.syExchangeRate
|
|
366
362
|
const ytValue = ptOutGrossU64 - syInBase
|
|
367
|
-
totalFeeOut =
|
|
363
|
+
totalFeeOut = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossU64, lpFeeRate)
|
|
368
364
|
} else {
|
|
369
365
|
totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
|
|
370
366
|
}
|
|
@@ -605,6 +601,8 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
605
601
|
|
|
606
602
|
const totalFeeOutBoundary = getFeeFromAmount(ptOutGrossBoundaryU64, lpFeeRate)
|
|
607
603
|
const ptOutNetBoundary = Math.max(0, ptOutGrossBoundaryU64 - totalFeeOutBoundary)
|
|
604
|
+
let boundarySegmentSeed: ReturnType<typeof calculateSwapSegment> | null = null
|
|
605
|
+
let ptOutNetBoundaryEffectiveSeed: number | null = null
|
|
608
606
|
|
|
609
607
|
if (ptOutNetBoundary === 0) {
|
|
610
608
|
const boundarySegment = calculateSwapSegment(
|
|
@@ -619,87 +617,115 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
619
617
|
args.isCurrentFlashSwap,
|
|
620
618
|
)
|
|
621
619
|
|
|
622
|
-
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
620
|
+
const ptOutNetBoundaryActual = Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
|
|
621
|
+
if (ptOutNetBoundaryActual > 0) {
|
|
622
|
+
boundarySegmentSeed = boundarySegment
|
|
623
|
+
ptOutNetBoundaryEffectiveSeed = ptOutNetBoundaryActual
|
|
624
|
+
} else {
|
|
625
|
+
let syInBoundary = boundarySegment.syInSegmentU64
|
|
626
|
+
if (ptOutGrossBoundaryU64 > 0 && syInBoundary === 0) {
|
|
627
|
+
syInBoundary = 1
|
|
628
|
+
}
|
|
626
629
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
) {
|
|
631
|
-
throw new Error("Insufficient SY budget for exact-out trade")
|
|
632
|
-
}
|
|
630
|
+
if (amountInConstraint !== undefined && amountInConsumed + syInBoundary > amountInConstraint) {
|
|
631
|
+
throw new Error("Insufficient SY budget for exact-out trade")
|
|
632
|
+
}
|
|
633
633
|
|
|
634
|
-
|
|
634
|
+
amountInConsumed += syInBoundary
|
|
635
635
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
636
|
+
if (boundarySegment.totalFeeOut > 0) {
|
|
637
|
+
const protocolFeeOut = Math.floor((boundarySegment.totalFeeOut * protocolFeeBps) / BASE_POINTS)
|
|
638
|
+
const lpFeeOut = boundarySegment.totalFeeOut - protocolFeeOut
|
|
639
|
+
feeLpOutU64 += lpFeeOut
|
|
640
|
+
feeProtocolOutU64 += protocolFeeOut
|
|
641
|
+
}
|
|
642
642
|
|
|
643
|
-
|
|
644
|
-
|
|
643
|
+
if (syInBoundary > 0 || boundarySegment.ptOutGrossActual > 0) {
|
|
644
|
+
ticksWrapper.setPrincipals(
|
|
645
|
+
currentLeftBoundaryKey,
|
|
646
|
+
principalPt - BigInt(boundarySegment.ptOutGrossActual),
|
|
647
|
+
principalSy + BigInt(syInBoundary),
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
currentPriceSpot = boundarySegment.spotPriceNew
|
|
652
|
+
|
|
653
|
+
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
645
654
|
currentLeftBoundaryKey,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
655
|
+
currentPriceSpot,
|
|
656
|
+
activeLiquidityU64,
|
|
657
|
+
activeLiquidityF64,
|
|
658
|
+
})
|
|
659
|
+
if (!crossed) break
|
|
660
|
+
currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
|
|
661
|
+
currentPriceSpot = crossed.currentPriceSpot
|
|
662
|
+
activeLiquidityU64 = crossed.activeLiquidityU64
|
|
663
|
+
activeLiquidityF64 = crossed.activeLiquidityF64
|
|
664
|
+
continue
|
|
649
665
|
}
|
|
666
|
+
}
|
|
650
667
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
668
|
+
let ptOutNetBoundaryEffective = ptOutNetBoundaryEffectiveSeed ?? ptOutNetBoundary
|
|
669
|
+
let precomputedBoundarySegment: ReturnType<typeof calculateSwapSegment> | null = null
|
|
670
|
+
if (amountOutLeft >= ptOutNetBoundary) {
|
|
671
|
+
const boundarySegment =
|
|
672
|
+
boundarySegmentSeed ??
|
|
673
|
+
calculateSwapSegment(
|
|
674
|
+
ptOutGrossBoundaryU64,
|
|
675
|
+
lTradeF64,
|
|
676
|
+
currentPriceSpot,
|
|
677
|
+
duToLeft,
|
|
678
|
+
anchorULeft,
|
|
679
|
+
snapshot,
|
|
680
|
+
cEffOld,
|
|
681
|
+
lpFeeRate,
|
|
682
|
+
args.isCurrentFlashSwap,
|
|
683
|
+
)
|
|
684
|
+
const ptOutNetBoundaryActual =
|
|
685
|
+
ptOutNetBoundaryEffectiveSeed ?? Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
|
|
686
|
+
ptOutNetBoundaryEffective = ptOutNetBoundaryActual
|
|
687
|
+
if (ptOutNetBoundaryActual > 0 && amountOutLeft >= ptOutNetBoundaryActual) {
|
|
688
|
+
precomputedBoundarySegment = boundarySegment
|
|
689
|
+
}
|
|
665
690
|
}
|
|
666
691
|
|
|
667
|
-
const targetNet = Math.min(amountOutLeft,
|
|
668
|
-
const grossCandidate =
|
|
669
|
-
|
|
670
|
-
|
|
692
|
+
const targetNet = Math.min(amountOutLeft, ptOutNetBoundaryEffective)
|
|
693
|
+
const grossCandidate = precomputedBoundarySegment
|
|
694
|
+
? ptOutGrossBoundaryU64
|
|
695
|
+
: findGrossForTargetNet(targetNet, Math.min(targetNet, ptOutGrossBoundaryU64), ptOutGrossBoundaryU64, (gross) =>
|
|
696
|
+
calculateNetPtOut(
|
|
697
|
+
gross,
|
|
698
|
+
lTradeF64,
|
|
699
|
+
currentPriceSpot,
|
|
700
|
+
duToLeft,
|
|
701
|
+
anchorULeft,
|
|
702
|
+
lpFeeRate,
|
|
703
|
+
args.isCurrentFlashSwap,
|
|
704
|
+
snapshot,
|
|
705
|
+
cEffOld,
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
const segment =
|
|
710
|
+
precomputedBoundarySegment ??
|
|
711
|
+
calculateSwapSegment(
|
|
712
|
+
grossCandidate,
|
|
671
713
|
lTradeF64,
|
|
672
714
|
currentPriceSpot,
|
|
673
715
|
duToLeft,
|
|
674
716
|
anchorULeft,
|
|
675
|
-
lpFeeRate,
|
|
676
|
-
args.isCurrentFlashSwap,
|
|
677
717
|
snapshot,
|
|
678
718
|
cEffOld,
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const segment = calculateSwapSegment(
|
|
683
|
-
grossCandidate,
|
|
684
|
-
lTradeF64,
|
|
685
|
-
currentPriceSpot,
|
|
686
|
-
duToLeft,
|
|
687
|
-
anchorULeft,
|
|
688
|
-
snapshot,
|
|
689
|
-
cEffOld,
|
|
690
|
-
lpFeeRate,
|
|
691
|
-
args.isCurrentFlashSwap,
|
|
692
|
-
)
|
|
719
|
+
lpFeeRate,
|
|
720
|
+
args.isCurrentFlashSwap,
|
|
721
|
+
)
|
|
693
722
|
|
|
694
723
|
let syInSegmentU64 = segment.syInSegmentU64
|
|
695
724
|
if (grossCandidate > 0 && syInSegmentU64 === 0) {
|
|
696
725
|
syInSegmentU64 = 1
|
|
697
726
|
}
|
|
698
727
|
|
|
699
|
-
if (
|
|
700
|
-
amountInConstraint !== undefined &&
|
|
701
|
-
amountInConsumed + syInSegmentU64 > amountInConstraint
|
|
702
|
-
) {
|
|
728
|
+
if (amountInConstraint !== undefined && amountInConsumed + syInSegmentU64 > amountInConstraint) {
|
|
703
729
|
throw new Error("Insufficient SY budget for exact-out trade")
|
|
704
730
|
}
|
|
705
731
|
|
|
@@ -781,27 +807,11 @@ function computeKappaAndLTrade(
|
|
|
781
807
|
let kappaPt: number
|
|
782
808
|
|
|
783
809
|
if (direction === SwapDirection.SyToPt) {
|
|
784
|
-
kappaPt = isSyOnlyRange
|
|
785
|
-
|
|
786
|
-
: ptMaxToLeftF > 0
|
|
787
|
-
? principalPtInInterval / ptMaxToLeftF
|
|
788
|
-
: Infinity
|
|
789
|
-
kappaSy = isPtOnlyRange
|
|
790
|
-
? 1
|
|
791
|
-
: yMaxToBoundaryF > 0
|
|
792
|
-
? principalSyInInterval / yMaxToBoundaryF
|
|
793
|
-
: Infinity
|
|
810
|
+
kappaPt = isSyOnlyRange ? 0 : ptMaxToLeftF > 0 ? principalPtInInterval / ptMaxToLeftF : Infinity
|
|
811
|
+
kappaSy = isPtOnlyRange ? 1 : yMaxToBoundaryF > 0 ? principalSyInInterval / yMaxToBoundaryF : Infinity
|
|
794
812
|
} else {
|
|
795
|
-
kappaSy = isPtOnlyRange
|
|
796
|
-
|
|
797
|
-
: yMaxToBoundaryF > 0
|
|
798
|
-
? principalSyInInterval / yMaxToBoundaryF
|
|
799
|
-
: Infinity
|
|
800
|
-
kappaPt = isSyOnlyRange
|
|
801
|
-
? 1
|
|
802
|
-
: ptMaxToLeftF > 0
|
|
803
|
-
? principalPtInInterval / ptMaxToLeftF
|
|
804
|
-
: Infinity
|
|
813
|
+
kappaSy = isPtOnlyRange ? 0 : yMaxToBoundaryF > 0 ? principalSyInInterval / yMaxToBoundaryF : Infinity
|
|
814
|
+
kappaPt = isSyOnlyRange ? 1 : ptMaxToLeftF > 0 ? principalPtInInterval / ptMaxToLeftF : Infinity
|
|
805
815
|
}
|
|
806
816
|
|
|
807
817
|
const kappa = Math.min(kappaPt, kappaSy, 1)
|
|
@@ -823,16 +833,94 @@ function findGrossForTargetNet(
|
|
|
823
833
|
maxGross: number,
|
|
824
834
|
calculateNet: (gross: number) => number,
|
|
825
835
|
): number {
|
|
836
|
+
const MAX_INTERPOLATION_STEPS = 6
|
|
837
|
+
const MAX_LOCAL_ADJUST_STEPS = 24
|
|
838
|
+
const MAX_BINARY_FALLBACK_STEPS = 12
|
|
839
|
+
|
|
826
840
|
if (minGross >= maxGross) {
|
|
827
841
|
return minGross
|
|
828
842
|
}
|
|
829
843
|
|
|
830
844
|
let left = minGross
|
|
845
|
+
let leftNet = calculateNet(left)
|
|
846
|
+
if (leftNet >= targetNet) {
|
|
847
|
+
return left
|
|
848
|
+
}
|
|
849
|
+
|
|
831
850
|
let right = maxGross
|
|
832
|
-
let
|
|
851
|
+
let rightNet = calculateNet(right)
|
|
852
|
+
if (rightNet <= targetNet) {
|
|
853
|
+
return right
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (let i = 0; i < MAX_INTERPOLATION_STEPS; i++) {
|
|
857
|
+
if (right <= left + 1) {
|
|
858
|
+
return left
|
|
859
|
+
}
|
|
860
|
+
if (rightNet <= leftNet) {
|
|
861
|
+
break
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const span = right - left
|
|
865
|
+
const targetDelta = Math.max(0, targetNet - leftNet)
|
|
866
|
+
const denominator = rightNet - leftNet
|
|
867
|
+
if (denominator === 0) {
|
|
868
|
+
break
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const estimatedOffset = Number((BigInt(targetDelta) * BigInt(span)) / BigInt(denominator))
|
|
872
|
+
let estimate = left + estimatedOffset
|
|
873
|
+
if (estimate <= left) {
|
|
874
|
+
estimate = left + 1
|
|
875
|
+
}
|
|
876
|
+
if (estimate >= right) {
|
|
877
|
+
estimate = right - 1
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const estimateNet = calculateNet(estimate)
|
|
881
|
+
if (estimateNet === targetNet) {
|
|
882
|
+
return estimate
|
|
883
|
+
}
|
|
884
|
+
if (estimateNet < targetNet) {
|
|
885
|
+
left = estimate
|
|
886
|
+
leftNet = estimateNet
|
|
887
|
+
} else {
|
|
888
|
+
right = estimate
|
|
889
|
+
rightNet = estimateNet
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
let best = left
|
|
894
|
+
for (let i = 0; i < MAX_LOCAL_ADJUST_STEPS; i++) {
|
|
895
|
+
if (best >= right) {
|
|
896
|
+
break
|
|
897
|
+
}
|
|
898
|
+
const next = best + 1
|
|
899
|
+
if (next > right) {
|
|
900
|
+
break
|
|
901
|
+
}
|
|
902
|
+
const nextNet = calculateNet(next)
|
|
903
|
+
if (nextNet > targetNet) {
|
|
904
|
+
break
|
|
905
|
+
}
|
|
906
|
+
best = next
|
|
907
|
+
if (nextNet === targetNet) {
|
|
908
|
+
return best
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (right <= best + 1) {
|
|
913
|
+
return best
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let low = best
|
|
917
|
+
let high = right
|
|
918
|
+
let bestGross = best
|
|
919
|
+
let steps = 0
|
|
833
920
|
|
|
834
|
-
while (
|
|
835
|
-
|
|
921
|
+
while (low <= high && steps < MAX_BINARY_FALLBACK_STEPS) {
|
|
922
|
+
steps += 1
|
|
923
|
+
const mid = low + Math.floor((high - low) / 2)
|
|
836
924
|
const net = calculateNet(mid)
|
|
837
925
|
|
|
838
926
|
if (net === targetNet) {
|
|
@@ -841,12 +929,12 @@ function findGrossForTargetNet(
|
|
|
841
929
|
|
|
842
930
|
if (net < targetNet) {
|
|
843
931
|
bestGross = mid
|
|
844
|
-
|
|
932
|
+
low = mid + 1
|
|
845
933
|
} else {
|
|
846
934
|
if (mid === 0) {
|
|
847
935
|
break
|
|
848
936
|
}
|
|
849
|
-
|
|
937
|
+
high = mid - 1
|
|
850
938
|
}
|
|
851
939
|
}
|
|
852
940
|
|
|
@@ -871,10 +959,7 @@ function calculateNetPtOut(
|
|
|
871
959
|
spotPriceNew = anchorULeft
|
|
872
960
|
}
|
|
873
961
|
|
|
874
|
-
const ptOutGrossActual = Math.max(
|
|
875
|
-
0,
|
|
876
|
-
Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
|
|
877
|
-
)
|
|
962
|
+
const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
|
|
878
963
|
|
|
879
964
|
const totalFeeOut = isCurrentFlashSwap
|
|
880
965
|
? (() => {
|
|
@@ -882,7 +967,7 @@ function calculateNetPtOut(
|
|
|
882
967
|
const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
|
|
883
968
|
const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
|
|
884
969
|
const ytValue = ptOutGrossActual - syInBase
|
|
885
|
-
return
|
|
970
|
+
return computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
|
|
886
971
|
})()
|
|
887
972
|
: getFeeFromAmount(ptOutGrossActual, lpFeeRate)
|
|
888
973
|
|
|
@@ -915,15 +1000,12 @@ function calculateSwapSegment(
|
|
|
915
1000
|
const cEffNew = snapshot.getEffectivePrice(spotPriceNew)
|
|
916
1001
|
const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
|
|
917
1002
|
const syInSegmentU64 = Math.max(0, Math.ceil(syInSegmentF))
|
|
918
|
-
const ptOutGrossActual = Math.max(
|
|
919
|
-
0,
|
|
920
|
-
Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
|
|
921
|
-
)
|
|
1003
|
+
const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
|
|
922
1004
|
|
|
923
1005
|
const normalFee = getFeeFromAmount(ptOutGrossActual, lpFeeRate)
|
|
924
1006
|
const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
|
|
925
1007
|
const ytValue = ptOutGrossActual - syInBase
|
|
926
|
-
const flashFee =
|
|
1008
|
+
const flashFee = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
|
|
927
1009
|
const totalFeeOut = isCurrentFlashSwap ? flashFee : normalFee
|
|
928
1010
|
|
|
929
1011
|
return {
|