@suilend/sdk 10.0.0 → 11.0.0
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/package.json +1 -1
- package/strategies.d.ts +15 -0
- package/strategies.js +259 -111
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@suilend/sdk","version":"
|
|
1
|
+
{"name":"@suilend/sdk","version":"11.0.0","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./client":"./client.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./api/events":"./api/events.js","./api":"./api/index.js","./lib/constants":"./lib/constants.js","./lib":"./lib/index.js","./lib/initialize":"./lib/initialize.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/proCompatible":"./lib/proCompatible.js","./lib/pyth":"./lib/pyth.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/transactions":"./lib/transactions.js","./lib/types":"./lib/types.js","./margin":"./margin/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers":"./parsers/index.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./parsers/obligation":"./parsers/obligation.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers/reserve":"./parsers/reserve.js","./swap":"./swap/index.js","./swap/quote":"./swap/quote.js","./swap/transaction":"./swap/transaction.js","./utils/events":"./utils/events.js","./utils/feedId":"./utils/feedId.js","./utils":"./utils/index.js","./utils/obligation":"./utils/obligation.js","./utils/simulate":"./utils/simulate.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend":"./_generated/suilend/index.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/permissions":"./margin/margin/permissions.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/version":"./margin/margin/version.js","./margin/utils":"./margin/utils/index.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/_dependencies/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js"},"types":"./index.d.ts","scripts":{"build":"rm -rf ./dist && tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json","test":"vitest run","lint:ci":"yarn run typecheck","prettier":"prettier --write src/ tests/","release":"yarn run build && node ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"^7.5.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^11.1.5","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"7.3.1","uuid":"^14.0.1"},"devDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.23.2","@suilend/sui-core":"^1.0.0","@tsconfig/recommended":"^1.0.8","@types/bn.js":"^5.2.0","@types/lodash":"^4.17.25","@types/node":"^26.1.2","fast-check":"^4.9.0","prettier":"^3.3.3","ts-node":"^10.9.2","typescript":"^6.0.3","vitest":"4.1.10"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":">=2.22.1 <3","@suilend/sui-core":"^1.0.0"},"type":"module"}
|
package/strategies.d.ts
CHANGED
|
@@ -11,6 +11,21 @@ import { RewardMap } from "./lib/liquidityMining";
|
|
|
11
11
|
import { StrategyType } from "./lib/strategyOwnerCap";
|
|
12
12
|
import { ParsedObligation, ParsedReserve } from "./parsers";
|
|
13
13
|
export declare const STRATEGY_E: number;
|
|
14
|
+
/**
|
|
15
|
+
* Below this, a levered position is uneconomic no matter what the slippage
|
|
16
|
+
* tolerance says. The aggregator's dust regime dominates: routes go multi-hop
|
|
17
|
+
* through thin venues, quote 0.3-1.3% above the honest single-hop rate and then
|
|
18
|
+
* deliver 2-8% below par, and a levered round trip runs ~20 of them — measured on
|
|
19
|
+
* mainnet, a 36-cent position at 4x lost 19% opening and closing. The one-cent
|
|
20
|
+
* absolute allowance that makes those swaps executable at all is itself 11-17% of
|
|
21
|
+
* a seven-cent swap.
|
|
22
|
+
*
|
|
23
|
+
* Refusing to OPEN one is the only guard that helps, because the alternative —
|
|
24
|
+
* capping the allowance so dust cannot authorise a large percentage — would strand
|
|
25
|
+
* every position already below the line with no way to exit. So the loose
|
|
26
|
+
* tolerance stays available on the unwind path, and entry is what gets blocked.
|
|
27
|
+
*/
|
|
28
|
+
export declare const STRATEGY_MIN_POSITION_USD = 10;
|
|
14
29
|
export declare const LST_DECIMALS = 9;
|
|
15
30
|
export type StrategyDeposit = {
|
|
16
31
|
coinType: string;
|
package/strategies.js
CHANGED
|
@@ -12,6 +12,107 @@ import { STRATEGY_TYPE_INFO_MAP, StrategyType, strategyBorrow, strategyClaimRewa
|
|
|
12
12
|
import { MMT_CONTRACT_PACKAGE_ID, MMT_VERSION_OBJECT_ID } from "./mmt.js";
|
|
13
13
|
import { getWeightedBorrowsUsd } from "./utils/index.js";
|
|
14
14
|
export const STRATEGY_E = 10 ** -7;
|
|
15
|
+
/**
|
|
16
|
+
* Cetus applies its slippage check against its OWN quote, so a wrong quote yields
|
|
17
|
+
* a wrong floor. Measured on a mainnet unwind of a sub-dollar position: the
|
|
18
|
+
* aggregator routed 4-9 cent swaps through 3-hop exotic venues, quoted them
|
|
19
|
+
* 0.3-1.3% ABOVE the honest single-hop rate, and the routes then delivered
|
|
20
|
+
* 2-8% BELOW par — a 5-9% gap between quote and execution. The inflation decays
|
|
21
|
+
* with size and is gone by ~0.12 of the sell asset, above which quotes are honest
|
|
22
|
+
* and single-hop.
|
|
23
|
+
*
|
|
24
|
+
* So the floor is anchored to the reserves' oracle prices — the same basis the
|
|
25
|
+
* obligation's own health math uses — and the tolerance is bounded in BOTH
|
|
26
|
+
* directions:
|
|
27
|
+
*
|
|
28
|
+
* allowance = max(relative, absolute)
|
|
29
|
+
*
|
|
30
|
+
* Dust swaps need real slack or they cannot execute at all, but that slack must
|
|
31
|
+
* not scale with size: a percentage wide enough for a 5-cent swap would authorise
|
|
32
|
+
* thousands of dollars of loss on a large unwind. Above ~1 USD of output the
|
|
33
|
+
* relative bound governs and holds the fill to 1%; below it the absolute bound
|
|
34
|
+
* does, and the most a step can give up is one cent.
|
|
35
|
+
*/
|
|
36
|
+
const SWAP_MAX_DEVIATION_PERCENT = 1;
|
|
37
|
+
const SWAP_MAX_DEVIATION_USD = 0.01;
|
|
38
|
+
/**
|
|
39
|
+
* Ceiling on the absolute term as a share of the reference output — oracle fair
|
|
40
|
+
* value for distinct-feed pairs, the quote itself for same-feed ones. Without
|
|
41
|
+
* it, an output worth less than `SWAP_MAX_DEVIATION_USD` drives the floor to
|
|
42
|
+
* zero, which makes `min_out` 1% of quote — the guard is not loose there, it is
|
|
43
|
+
* off, and the implausibility check cannot see it because an honest sub-cent
|
|
44
|
+
* quote sits right at fair value. Grandfathered sub-minimum positions are the
|
|
45
|
+
* ones that reach it, on the unwind path deliberately left open so they can
|
|
46
|
+
* exit.
|
|
47
|
+
*/
|
|
48
|
+
const SWAP_MAX_ALLOWANCE_FRACTION_OF_REFERENCE = 0.5;
|
|
49
|
+
const SWAP_MIN_SLIPPAGE_PERCENT = 0.1;
|
|
50
|
+
const SWAP_MAX_SLIPPAGE_PERCENT = 99;
|
|
51
|
+
const SWAP_MAX_QUOTE_EXCESS_PERCENT = 50;
|
|
52
|
+
/**
|
|
53
|
+
* Below this, a levered position is uneconomic no matter what the slippage
|
|
54
|
+
* tolerance says. The aggregator's dust regime dominates: routes go multi-hop
|
|
55
|
+
* through thin venues, quote 0.3-1.3% above the honest single-hop rate and then
|
|
56
|
+
* deliver 2-8% below par, and a levered round trip runs ~20 of them — measured on
|
|
57
|
+
* mainnet, a 36-cent position at 4x lost 19% opening and closing. The one-cent
|
|
58
|
+
* absolute allowance that makes those swaps executable at all is itself 11-17% of
|
|
59
|
+
* a seven-cent swap.
|
|
60
|
+
*
|
|
61
|
+
* Refusing to OPEN one is the only guard that helps, because the alternative —
|
|
62
|
+
* capping the allowance so dust cannot authorise a large percentage — would strand
|
|
63
|
+
* every position already below the line with no way to exit. So the loose
|
|
64
|
+
* tolerance stays available on the unwind path, and entry is what gets blocked.
|
|
65
|
+
*/
|
|
66
|
+
export const STRATEGY_MIN_POSITION_USD = 10;
|
|
67
|
+
const resolveSwapSlippage = (amountIn, quotedAmountOut, sellReserve, buyReserve) => {
|
|
68
|
+
if (quotedAmountOut.lte(0))
|
|
69
|
+
throw new Error("No swap quote found");
|
|
70
|
+
// Two reserves reading the SAME feed have a price ratio of 1.0 by
|
|
71
|
+
// construction, which says nothing about the rate between them. eEARN is
|
|
72
|
+
// pinned to USDC's feed while trading ~3.6% above it, so anchoring there
|
|
73
|
+
// would put the floor above every honest quote and refuse the swap outright —
|
|
74
|
+
// size-independent, unrelated to the dust regime this guard is for. Anchor the
|
|
75
|
+
// dual bound on the quote instead: it is the absolute allowance, not the
|
|
76
|
+
// anchor, that makes a dust swap executable.
|
|
77
|
+
//
|
|
78
|
+
// This is a stopgap, not the answer: repointing eEARN to a DIFFERENT wrong
|
|
79
|
+
// feed makes the identifiers differ while the pin stays wrong. The durable fix
|
|
80
|
+
// is to anchor on an aggregator quote taken at a size where quotes are honest,
|
|
81
|
+
// using the oracle only to choose that size. Tracked as a follow-up.
|
|
82
|
+
const sameFeed = sellReserve.priceIdentifier === buyReserve.priceIdentifier;
|
|
83
|
+
// Sell side at its low price, buy side at its high price: the fair value errs
|
|
84
|
+
// conservative in the direction that protects the position.
|
|
85
|
+
const referenceAmountOut = sameFeed
|
|
86
|
+
? quotedAmountOut
|
|
87
|
+
: amountIn.times(sellReserve.minPrice).div(buyReserve.maxPrice);
|
|
88
|
+
const allowance = BigNumber.min(BigNumber.max(referenceAmountOut.times(SWAP_MAX_DEVIATION_PERCENT / 100), new BigNumber(SWAP_MAX_DEVIATION_USD).div(buyReserve.maxPrice)), referenceAmountOut.times(SWAP_MAX_ALLOWANCE_FRACTION_OF_REFERENCE));
|
|
89
|
+
const floorAmountOut = BigNumber.max(new BigNumber(0), referenceAmountOut.minus(allowance));
|
|
90
|
+
// Quoting below the floor means the route cannot fill at a price worth taking.
|
|
91
|
+
// Refuse rather than build a swap that trades the position down.
|
|
92
|
+
if (quotedAmountOut.lt(floorAmountOut))
|
|
93
|
+
throw new Error("Swap quote is below the oracle floor");
|
|
94
|
+
// A quote far above the HIGH plausible output means the anchor is wrong, not
|
|
95
|
+
// that the trade is good — a pin that overstates the bought asset collapses
|
|
96
|
+
// the floor. Compare against maxPrice/minPrice rather than the floor's own
|
|
97
|
+
// basis: reusing the low bound makes this tightest exactly when volatility
|
|
98
|
+
// widens min/max, which is when a deleverage has to get through. Same-feed
|
|
99
|
+
// pairs have nothing independent to compare against, so they skip it.
|
|
100
|
+
if (!sameFeed) {
|
|
101
|
+
const fairUpperAmountOut = amountIn
|
|
102
|
+
.times(sellReserve.maxPrice)
|
|
103
|
+
.div(buyReserve.minPrice);
|
|
104
|
+
if (quotedAmountOut.gt(fairUpperAmountOut.times(1 + SWAP_MAX_QUOTE_EXCESS_PERCENT / 100)))
|
|
105
|
+
throw new Error("Swap quote is implausible against the oracle price");
|
|
106
|
+
}
|
|
107
|
+
const slippagePercent = Math.min(Math.max(new BigNumber(1)
|
|
108
|
+
.minus(floorAmountOut.div(quotedAmountOut))
|
|
109
|
+
.times(100)
|
|
110
|
+
.toNumber(), SWAP_MIN_SLIPPAGE_PERCENT), SWAP_MAX_SLIPPAGE_PERCENT);
|
|
111
|
+
return {
|
|
112
|
+
slippagePercent,
|
|
113
|
+
guaranteedAmountOut: quotedAmountOut.times(1 - slippagePercent / 100),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
15
116
|
export const LST_DECIMALS = 9;
|
|
16
117
|
export var StrategyFlashLoanProvider;
|
|
17
118
|
(function (StrategyFlashLoanProvider) {
|
|
@@ -1050,7 +1151,7 @@ transaction) => {
|
|
|
1050
1151
|
});
|
|
1051
1152
|
if (!routers)
|
|
1052
1153
|
throw new Error("No swap quote found");
|
|
1053
|
-
const slippagePercent =
|
|
1154
|
+
const { slippagePercent, guaranteedAmountOut } = resolveSwapSlippage(stepBorrowedAmount, new BigNumber(routers.amountOut.toString()).div(10 ** loopingDepositReserve.token.decimals), borrowReserve, loopingDepositReserve);
|
|
1054
1155
|
let stepBaseCoin;
|
|
1055
1156
|
try {
|
|
1056
1157
|
stepBaseCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -1074,7 +1175,14 @@ transaction) => {
|
|
|
1074
1175
|
.toFixed(20),
|
|
1075
1176
|
}, null, 2), routers);
|
|
1076
1177
|
// 2.2) Deposit
|
|
1077
|
-
|
|
1178
|
+
// Credit the GUARANTEED output, not the quote. The borrow is exact but the
|
|
1179
|
+
// swap output is not, and the aggregator inflates dust quotes — on a mainnet
|
|
1180
|
+
// loop-up it claimed a 2.5% gain buying an asset that costs slightly over
|
|
1181
|
+
// par. Booking the quote makes the simulated deposit larger than the real
|
|
1182
|
+
// one while the borrow stands, so each step borrows against collateral that
|
|
1183
|
+
// never arrives and `obligation::borrow` aborts with EObligationIsNotHealthy.
|
|
1184
|
+
// Erring low leaves the position slightly under target exposure instead.
|
|
1185
|
+
const stepDepositedAmount = guaranteedAmountOut.decimalPlaces(loopingDepositReserve.token.decimals, BigNumber.ROUND_DOWN);
|
|
1078
1186
|
const isMaxDeposit = stepDepositedAmount.eq(stepMaxDepositedAmount);
|
|
1079
1187
|
console.log(`[loopStrategyToExposure] ${i} deposit.deposit |`, JSON.stringify({
|
|
1080
1188
|
stepDepositedAmount: stepDepositedAmount.toFixed(20),
|
|
@@ -1376,8 +1484,25 @@ transaction, dryRunTransaction) => {
|
|
|
1376
1484
|
.toString()),
|
|
1377
1485
|
byAmountIn: true,
|
|
1378
1486
|
});
|
|
1379
|
-
if (!routers)
|
|
1380
|
-
|
|
1487
|
+
if (!routers) {
|
|
1488
|
+
transaction = transactionBeforeFullRepay;
|
|
1489
|
+
deposits = depositsBeforeFullRepay;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
let fullRepaySlippagePercent;
|
|
1493
|
+
try {
|
|
1494
|
+
({ slippagePercent: fullRepaySlippagePercent } = resolveSwapSlippage(baseWithdrawnAmount, new BigNumber(routers.amountOut.toString()).div(10 ** borrowReserve.token.decimals), depositReserves.base, borrowReserve));
|
|
1495
|
+
}
|
|
1496
|
+
catch (err) {
|
|
1497
|
+
// A route that cannot fill is the same failure whether it is caught before
|
|
1498
|
+
// or after the swap is appended. Undo the handler and leave the debt on the
|
|
1499
|
+
// books, exactly as its repay `catch` below does — the base-only caller
|
|
1500
|
+
// breaks straight after this with no `catch` of its own.
|
|
1501
|
+
console.error(err);
|
|
1502
|
+
transaction = transactionBeforeFullRepay;
|
|
1503
|
+
deposits = depositsBeforeFullRepay;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1381
1506
|
console.log(`[unloopStrategyToExposure.fullyRepayBorrowsUsingBase] swap_base_for_borrows.get_routers`, {
|
|
1382
1507
|
routers,
|
|
1383
1508
|
amountIn: new BigNumber(routers.amountIn.toString())
|
|
@@ -1395,13 +1520,16 @@ transaction, dryRunTransaction) => {
|
|
|
1395
1520
|
swapCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
1396
1521
|
router: routers,
|
|
1397
1522
|
inputCoin: withdrawnBaseCoin,
|
|
1398
|
-
slippage:
|
|
1523
|
+
slippage: fullRepaySlippagePercent / 100,
|
|
1399
1524
|
txb: transaction,
|
|
1400
1525
|
partner: cetusPartnerId,
|
|
1401
1526
|
}));
|
|
1402
1527
|
}
|
|
1403
1528
|
catch (err) {
|
|
1404
|
-
|
|
1529
|
+
console.error(err);
|
|
1530
|
+
transaction = transactionBeforeFullRepay;
|
|
1531
|
+
deposits = depositsBeforeFullRepay;
|
|
1532
|
+
return;
|
|
1405
1533
|
}
|
|
1406
1534
|
// 4) Repay borrows
|
|
1407
1535
|
// 4.1) Repay
|
|
@@ -1644,12 +1772,11 @@ transaction, dryRunTransaction) => {
|
|
|
1644
1772
|
stepMaxWithdrawnAmount: stepMaxWithdrawnAmount.toFixed(20),
|
|
1645
1773
|
stepMaxRepaidAmount: stepMaxRepaidAmount.toFixed(20),
|
|
1646
1774
|
}, null, 2));
|
|
1647
|
-
const slippagePercent = 1;
|
|
1648
1775
|
// 1.2) Withdraw
|
|
1649
1776
|
// Once this step's ceiling can cover everything still owed, it is the last
|
|
1650
1777
|
// one — so size it to clear the debt outright rather than to the exact tracked
|
|
1651
|
-
// figure. The swap may fill
|
|
1652
|
-
//
|
|
1778
|
+
// figure. The swap may fill as low as the oracle floor, so scale by that same
|
|
1779
|
+
// bound to stay covered at the worst fill the guard accepts. Overshooting is free:
|
|
1653
1780
|
// `obligation::repay` clamps to what is actually owed
|
|
1654
1781
|
// (`min(max_repay_amount, borrowed_amount)`) and the surplus is transferred
|
|
1655
1782
|
// back to the user below.
|
|
@@ -1659,7 +1786,7 @@ transaction, dryRunTransaction) => {
|
|
|
1659
1786
|
// the repay step). The next iteration then aborts with `EBorrowNotFound` — a
|
|
1660
1787
|
// fully repaid borrow is removed from the obligation, so there is nothing left
|
|
1661
1788
|
// to repay against.
|
|
1662
|
-
const stepTargetRepaidAmount = pendingBorrowedAmount.div(1 -
|
|
1789
|
+
const stepTargetRepaidAmount = pendingBorrowedAmount.div(1 - SWAP_MAX_DEVIATION_PERCENT / 100);
|
|
1663
1790
|
const isFinalStep = stepTargetRepaidAmount.lte(stepMaxRepaidAmount);
|
|
1664
1791
|
const stepWithdrawnAmount = BigNumber.min(stepTargetRepaidAmount, stepMaxRepaidAmount)
|
|
1665
1792
|
.div(baseToBorrowExchangeRate)
|
|
@@ -1683,110 +1810,120 @@ transaction, dryRunTransaction) => {
|
|
|
1683
1810
|
// whole.
|
|
1684
1811
|
const transactionBeforeStep = Transaction.from(transaction);
|
|
1685
1812
|
const depositsBeforeStep = cloneDeep(deposits);
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
//
|
|
1694
|
-
deposits = addOrInsertStrategyDeposit(deposits, {
|
|
1695
|
-
coinType: loopingDepositReserve.coinType,
|
|
1696
|
-
depositedAmount: stepWithdrawnAmount.times(-1),
|
|
1697
|
-
});
|
|
1698
|
-
console.log(`[unloopStrategyToExposure] ${i} withdraw_base.update_state |`, JSON.stringify({
|
|
1699
|
-
deposits: deposits.map((d) => ({
|
|
1700
|
-
coinType: d.coinType,
|
|
1701
|
-
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1702
|
-
})),
|
|
1703
|
-
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1704
|
-
}, null, 2));
|
|
1705
|
-
// 2) Swap base for borrows
|
|
1706
|
-
const routers = await cetusSdk.findRouters({
|
|
1707
|
-
from: loopingDepositReserve.coinType,
|
|
1708
|
-
target: borrowReserve.coinType,
|
|
1709
|
-
amount: new BN(stepWithdrawnAmount
|
|
1710
|
-
.times(10 ** loopingDepositReserve.token.decimals)
|
|
1711
|
-
.integerValue(BigNumber.ROUND_DOWN)
|
|
1712
|
-
.toString()), // Estimate for loop 2 onwards (don't know exact out amount, we are not accounting for swap fees, etc)
|
|
1713
|
-
byAmountIn: true,
|
|
1714
|
-
splitCount: 0, // Use direct swap to avoid split algo
|
|
1715
|
-
});
|
|
1716
|
-
if (!routers)
|
|
1717
|
-
throw new Error("No swap quote found");
|
|
1718
|
-
let stepBorrowCoin;
|
|
1719
|
-
try {
|
|
1720
|
-
stepBorrowCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
1721
|
-
router: routers,
|
|
1722
|
-
inputCoin: stepWithdrawnCoin,
|
|
1723
|
-
slippage: slippagePercent / 100,
|
|
1724
|
-
txb: transaction,
|
|
1725
|
-
partner: cetusPartnerId,
|
|
1726
|
-
}));
|
|
1727
|
-
}
|
|
1728
|
-
catch (err) {
|
|
1729
|
-
throw new Error("No swap quote found");
|
|
1730
|
-
}
|
|
1731
|
-
console.log(`[unloopStrategyToExposure] ${i} swap_base_for_borrows |`, JSON.stringify({
|
|
1732
|
-
inCoinType: loopingDepositReserve.coinType,
|
|
1733
|
-
outCoinType: borrowReserve.coinType,
|
|
1734
|
-
amountIn: stepWithdrawnAmount.toFixed(20),
|
|
1735
|
-
amountOut: new BigNumber(routers.amountOut.toString())
|
|
1736
|
-
.div(10 ** borrowReserve.token.decimals)
|
|
1737
|
-
.decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN)
|
|
1738
|
-
.toFixed(20),
|
|
1739
|
-
}, null, 2), routers);
|
|
1740
|
-
// 3) Repay borrows
|
|
1741
|
-
// 3.1) Repay
|
|
1742
|
-
// Actual on-chain swap output can be up to `slippagePercent`% less than the
|
|
1743
|
-
// Cetus quote — track pessimistically so `borrowedAmount` stays ≥ real debt.
|
|
1744
|
-
// Otherwise per-step under-counting accumulates and the final
|
|
1745
|
-
// `fullyRepayBorrowsUsingBase` sizes off a near-zero tracked value, stranding
|
|
1746
|
-
// dust borrow that blocks the subsequent `MAX_U64` deposit withdraw.
|
|
1747
|
-
const stepRepaidAmount = new BigNumber(new BigNumber(routers.amountOut.toString())
|
|
1748
|
-
.times(1 - slippagePercent / 100)
|
|
1749
|
-
.div(10 ** borrowReserve.token.decimals)).decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN);
|
|
1750
|
-
const isMaxRepay = stepRepaidAmount.eq(stepMaxRepaidAmount);
|
|
1751
|
-
console.log(`[unloopStrategyToExposure] ${i} repay_borrows.repay |`, JSON.stringify({
|
|
1752
|
-
stepRepaidAmount: stepRepaidAmount.toFixed(20),
|
|
1753
|
-
isMaxRepay,
|
|
1754
|
-
}, null, 2));
|
|
1813
|
+
// Every failure inside a step wants the same handling: undo the step and
|
|
1814
|
+
// stop. A missing route, a quote below the oracle floor, a swap that will
|
|
1815
|
+
// not build, a repay whose dry run aborts — all of them leave the withdraw
|
|
1816
|
+
// and the swap already appended to `transaction` and `deposits` already
|
|
1817
|
+
// decremented, so anything that escapes this block escapes the snapshot too
|
|
1818
|
+
// and takes the whole operation down instead of ending it with a partial
|
|
1819
|
+
// unwind. One catch makes that invariant explicit: nothing in a step
|
|
1820
|
+
// survives that step failing.
|
|
1755
1821
|
let stepClearedDebt = false;
|
|
1756
1822
|
try {
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
//
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1823
|
+
const [stepWithdrawnCoin] = strategyWithdraw(strategyType, loopingDepositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(loopingDepositReserve.coinType), BigInt(new BigNumber(stepWithdrawnAmount
|
|
1824
|
+
.times(10 ** loopingDepositReserve.token.decimals)
|
|
1825
|
+
.integerValue(BigNumber.ROUND_DOWN)
|
|
1826
|
+
.toString())
|
|
1827
|
+
.div(loopingDepositReserve.cTokenExchangeRate)
|
|
1828
|
+
.integerValue(BigNumber.ROUND_UP)
|
|
1829
|
+
.toString()), transaction);
|
|
1830
|
+
// 1.3) Update state
|
|
1831
|
+
deposits = addOrInsertStrategyDeposit(deposits, {
|
|
1832
|
+
coinType: loopingDepositReserve.coinType,
|
|
1833
|
+
depositedAmount: stepWithdrawnAmount.times(-1),
|
|
1834
|
+
});
|
|
1835
|
+
console.log(`[unloopStrategyToExposure] ${i} withdraw_base.update_state |`, JSON.stringify({
|
|
1836
|
+
deposits: deposits.map((d) => ({
|
|
1837
|
+
coinType: d.coinType,
|
|
1838
|
+
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1839
|
+
})),
|
|
1840
|
+
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1841
|
+
}, null, 2));
|
|
1842
|
+
// 2) Swap base for borrows
|
|
1843
|
+
const routers = await cetusSdk.findRouters({
|
|
1844
|
+
from: loopingDepositReserve.coinType,
|
|
1845
|
+
target: borrowReserve.coinType,
|
|
1846
|
+
amount: new BN(stepWithdrawnAmount
|
|
1847
|
+
.times(10 ** loopingDepositReserve.token.decimals)
|
|
1848
|
+
.integerValue(BigNumber.ROUND_DOWN)
|
|
1849
|
+
.toString()), // Estimate for loop 2 onwards (don't know exact out amount, we are not accounting for swap fees, etc)
|
|
1850
|
+
byAmountIn: true,
|
|
1851
|
+
splitCount: 0, // Use direct swap to avoid split algo
|
|
1852
|
+
});
|
|
1853
|
+
if (!routers)
|
|
1854
|
+
throw new Error("No swap quote found");
|
|
1855
|
+
const quotedAmountOut = new BigNumber(routers.amountOut.toString()).div(10 ** borrowReserve.token.decimals);
|
|
1856
|
+
const { slippagePercent, guaranteedAmountOut } = resolveSwapSlippage(stepWithdrawnAmount, quotedAmountOut, loopingDepositReserve, borrowReserve);
|
|
1857
|
+
console.log(`[unloopStrategyToExposure] ${i} swap_base_for_borrows.floor |`, JSON.stringify({
|
|
1858
|
+
quotedRate: quotedAmountOut.div(stepWithdrawnAmount).toFixed(6),
|
|
1859
|
+
// What the pool must actually deliver, per unit of input.
|
|
1860
|
+
requiredRate: guaranteedAmountOut
|
|
1861
|
+
.div(stepWithdrawnAmount)
|
|
1862
|
+
.toFixed(6),
|
|
1863
|
+
slippagePassedToCetus: slippagePercent.toFixed(4),
|
|
1864
|
+
}, null, 2));
|
|
1865
|
+
let stepBorrowCoin;
|
|
1866
|
+
{
|
|
1867
|
+
stepBorrowCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
1868
|
+
router: routers,
|
|
1869
|
+
inputCoin: stepWithdrawnCoin,
|
|
1870
|
+
slippage: slippagePercent / 100,
|
|
1871
|
+
txb: transaction,
|
|
1872
|
+
partner: cetusPartnerId,
|
|
1873
|
+
}));
|
|
1874
|
+
}
|
|
1875
|
+
console.log(`[unloopStrategyToExposure] ${i} swap_base_for_borrows |`, JSON.stringify({
|
|
1876
|
+
inCoinType: loopingDepositReserve.coinType,
|
|
1877
|
+
outCoinType: borrowReserve.coinType,
|
|
1878
|
+
amountIn: stepWithdrawnAmount.toFixed(20),
|
|
1879
|
+
amountOut: new BigNumber(routers.amountOut.toString())
|
|
1880
|
+
.div(10 ** borrowReserve.token.decimals)
|
|
1881
|
+
.decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN)
|
|
1882
|
+
.toFixed(20),
|
|
1883
|
+
}, null, 2), routers);
|
|
1884
|
+
// 3) Repay borrows
|
|
1885
|
+
// 3.1) Repay
|
|
1886
|
+
// The guaranteed output is exactly what `min_out` enforces, so tracking it
|
|
1887
|
+
// keeps `borrowedAmount` ≥ the real debt without the arbitrary haircut that
|
|
1888
|
+
// used to sit here. Under-counting would accumulate and leave
|
|
1889
|
+
// `fullyRepayBorrowsUsingBase` sizing off a near-zero tracked value,
|
|
1890
|
+
// stranding dust borrow that blocks the subsequent `MAX_U64` withdraw.
|
|
1891
|
+
const stepRepaidAmount = guaranteedAmountOut.decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN);
|
|
1892
|
+
const isMaxRepay = stepRepaidAmount.eq(stepMaxRepaidAmount);
|
|
1893
|
+
console.log(`[unloopStrategyToExposure] ${i} repay_borrows.repay |`, JSON.stringify({
|
|
1894
|
+
stepRepaidAmount: stepRepaidAmount.toFixed(20),
|
|
1895
|
+
isMaxRepay,
|
|
1896
|
+
}, null, 2));
|
|
1897
|
+
{
|
|
1898
|
+
const txCopy = Transaction.from(transaction);
|
|
1899
|
+
suilendClient.repay(obligationId, borrowReserve.coinType, stepBorrowCoin, txCopy);
|
|
1900
|
+
txCopy.transferObjects([stepBorrowCoin], _address);
|
|
1901
|
+
await dryRunTransaction(txCopy); // Throws error if fails
|
|
1902
|
+
transaction = txCopy;
|
|
1903
|
+
// 3.2) Update state
|
|
1904
|
+
// A final step was sized to clear the debt even at the worst fill, and the
|
|
1905
|
+
// repay clamps, so the target is reached exactly rather than by subtracting a
|
|
1906
|
+
// figure the haircut has already made fictional.
|
|
1907
|
+
borrowedAmount = isFinalStep
|
|
1908
|
+
? targetBorrowedAmount
|
|
1909
|
+
: borrowedAmount.minus(stepRepaidAmount);
|
|
1910
|
+
stepClearedDebt = isFinalStep;
|
|
1911
|
+
}
|
|
1912
|
+
console.log(`[unloopStrategyToExposure] ${i} repay_borrows.update_state |`, JSON.stringify({
|
|
1913
|
+
deposits: deposits.map((d) => ({
|
|
1914
|
+
coinType: d.coinType,
|
|
1915
|
+
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1916
|
+
})),
|
|
1917
|
+
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1918
|
+
stepClearedDebt,
|
|
1919
|
+
}, null, 2));
|
|
1770
1920
|
}
|
|
1771
1921
|
catch (err) {
|
|
1772
|
-
// The step cannot land — the swap's slippage check aborted, or the borrow is
|
|
1773
|
-
// already gone (`EBorrowNotFound`). Roll the whole step back either way:
|
|
1774
|
-
// keeping it would leave an aborting command in the transaction, so every
|
|
1775
|
-
// later dry run would fail on that same command and the user would be asked
|
|
1776
|
-
// to sign a transaction known to fail.
|
|
1777
1922
|
console.error(err);
|
|
1778
1923
|
transaction = transactionBeforeStep;
|
|
1779
1924
|
deposits = depositsBeforeStep;
|
|
1780
1925
|
break;
|
|
1781
1926
|
}
|
|
1782
|
-
console.log(`[unloopStrategyToExposure] ${i} repay_borrows.update_state |`, JSON.stringify({
|
|
1783
|
-
deposits: deposits.map((d) => ({
|
|
1784
|
-
coinType: d.coinType,
|
|
1785
|
-
depositedAmount: d.depositedAmount.toFixed(20),
|
|
1786
|
-
})),
|
|
1787
|
-
borrowedAmount: borrowedAmount.toFixed(20),
|
|
1788
|
-
stepClearedDebt,
|
|
1789
|
-
}, null, 2));
|
|
1790
1927
|
// Nothing is owed above target any more. Returning here rather than looping
|
|
1791
1928
|
// again also skips `fullyRepayBorrowsUsingBase`, which would repay a borrow
|
|
1792
1929
|
// that no longer exists.
|
|
@@ -1889,7 +2026,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
1889
2026
|
});
|
|
1890
2027
|
if (!routers)
|
|
1891
2028
|
throw new Error("No swap quote found");
|
|
1892
|
-
const slippagePercent =
|
|
2029
|
+
const { slippagePercent } = resolveSwapSlippage(deposit.depositedAmount, new BigNumber(routers.amountOut.toString()).div(10 ** depositReserve.token.decimals), otherReserve, depositReserve);
|
|
1893
2030
|
let baseCoin;
|
|
1894
2031
|
try {
|
|
1895
2032
|
baseCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -1956,6 +2093,14 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
1956
2093
|
targetExposure: targetExposure.toFixed(20),
|
|
1957
2094
|
}, null, 2));
|
|
1958
2095
|
//
|
|
2096
|
+
// Entry only, and only where leverage is actually taken: the guard exists
|
|
2097
|
+
// because a levered round trip runs ~20 dust swaps, and 1x runs at most one
|
|
2098
|
+
// conversion — where a one-cent allowance is a fraction of a percent, nowhere
|
|
2099
|
+
// near the regime this refuses. An existing position below the line can still
|
|
2100
|
+
// be adjusted and unwound either way.
|
|
2101
|
+
const resultingTvlUsd = getStrategyTvlAmount(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, addOrInsertStrategyDeposit(cloneDeep(_deposits), deposit), _borrowedAmount)).times(defaultCurrencyReserve.minPrice);
|
|
2102
|
+
if (targetExposure.gt(1) && resultingTvlUsd.lt(STRATEGY_MIN_POSITION_USD))
|
|
2103
|
+
throw new Error(`Position must be at least $${STRATEGY_MIN_POSITION_USD} to open`);
|
|
1959
2104
|
let deposits = cloneDeep(_deposits);
|
|
1960
2105
|
let borrowedAmount = _borrowedAmount;
|
|
1961
2106
|
// 1) Deposit (1x exposure)
|
|
@@ -2117,7 +2262,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2117
2262
|
});
|
|
2118
2263
|
if (!routers)
|
|
2119
2264
|
throw new Error("No swap quote found");
|
|
2120
|
-
const slippagePercent =
|
|
2265
|
+
const { slippagePercent } = resolveSwapSlippage(finalWithdrawnAmount, new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdraw.coinType].token.decimals), depositReserve, reserveMap[withdraw.coinType]);
|
|
2121
2266
|
let swappedCoin;
|
|
2122
2267
|
try {
|
|
2123
2268
|
swappedCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -2232,7 +2377,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2232
2377
|
});
|
|
2233
2378
|
if (!routers)
|
|
2234
2379
|
throw new Error("No swap quote found");
|
|
2235
|
-
const slippagePercent =
|
|
2380
|
+
const { slippagePercent } = resolveSwapSlippage(estimatedWithdrawnAmount, new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdrawCoinType].token.decimals), depositReserve, reserveMap[withdrawCoinType]);
|
|
2236
2381
|
let swappedCoin;
|
|
2237
2382
|
try {
|
|
2238
2383
|
swappedCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -2373,12 +2518,13 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2373
2518
|
.toFixed(20),
|
|
2374
2519
|
});
|
|
2375
2520
|
// 2.2) Swap
|
|
2521
|
+
const { slippagePercent: adjustRepaySlippagePercent } = resolveSwapSlippage(new BigNumber(routers.amountIn.toString()).div(10 ** depositReserves.base.token.decimals), new BigNumber(routers.amountOut.toString()).div(10 ** borrowReserve.token.decimals), depositReserves.base, borrowReserve);
|
|
2376
2522
|
let swapCoin;
|
|
2377
2523
|
try {
|
|
2378
2524
|
swapCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
2379
2525
|
router: routers,
|
|
2380
2526
|
inputCoin: flashLoanBorrowedCoin,
|
|
2381
|
-
slippage:
|
|
2527
|
+
slippage: adjustRepaySlippagePercent / 100,
|
|
2382
2528
|
txb: transaction,
|
|
2383
2529
|
partner: cetusPartnerId,
|
|
2384
2530
|
}));
|
|
@@ -2559,11 +2705,12 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2559
2705
|
});
|
|
2560
2706
|
if (!depositRouters)
|
|
2561
2707
|
throw new Error("No swap quote found");
|
|
2708
|
+
const { slippagePercent: depositLegSlippagePercent } = resolveSwapSlippage(new BigNumber(depositRouters.amountIn.toString()).div(10 ** flashLoanCoinReserve.token.decimals), new BigNumber(depositRouters.amountOut.toString()).div(10 ** depositReserve.token.decimals), flashLoanCoinReserve, depositReserve);
|
|
2562
2709
|
try {
|
|
2563
2710
|
flashLoanBorrowedCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
2564
2711
|
router: depositRouters,
|
|
2565
2712
|
inputCoin: flashLoanBorrowedCoin,
|
|
2566
|
-
slippage:
|
|
2713
|
+
slippage: depositLegSlippagePercent / 100,
|
|
2567
2714
|
txb: transaction,
|
|
2568
2715
|
partner: cetusPartnerId,
|
|
2569
2716
|
}));
|
|
@@ -2636,11 +2783,12 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2636
2783
|
});
|
|
2637
2784
|
if (!repayRouters)
|
|
2638
2785
|
throw new Error("No swap quote found");
|
|
2786
|
+
const { slippagePercent: repayLegSlippagePercent } = resolveSwapSlippage(new BigNumber(repayRouters.amountIn.toString()).div(10 ** depositReserve.token.decimals), new BigNumber(repayRouters.amountOut.toString()).div(10 ** flashLoanCoinReserve.token.decimals), depositReserve, flashLoanCoinReserve);
|
|
2639
2787
|
try {
|
|
2640
2788
|
flashLoanRepayCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
2641
2789
|
router: repayRouters,
|
|
2642
2790
|
inputCoin: flashLoanRepayCoin,
|
|
2643
|
-
slippage:
|
|
2791
|
+
slippage: repayLegSlippagePercent / 100,
|
|
2644
2792
|
txb: transaction,
|
|
2645
2793
|
partner: cetusPartnerId,
|
|
2646
2794
|
}));
|