@strkfarm/sdk 2.0.0-dev.27 → 2.0.0-dev.28
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/cli.js +190 -36
- package/dist/cli.mjs +188 -34
- package/dist/index.browser.global.js +79130 -49357
- package/dist/index.browser.mjs +18039 -11434
- package/dist/index.d.ts +2869 -898
- package/dist/index.js +19036 -12210
- package/dist/index.mjs +18942 -12161
- package/package.json +1 -1
- package/src/data/avnu.abi.json +840 -0
- package/src/data/ekubo-price-fethcer.abi.json +265 -0
- package/src/dataTypes/_bignumber.ts +13 -4
- package/src/dataTypes/index.ts +3 -2
- package/src/dataTypes/mynumber.ts +141 -0
- package/src/global.ts +76 -41
- package/src/index.browser.ts +2 -1
- package/src/interfaces/common.tsx +167 -2
- package/src/modules/ExtendedWrapperSDk/types.ts +26 -4
- package/src/modules/ExtendedWrapperSDk/wrapper.ts +110 -67
- package/src/modules/apollo-client-config.ts +28 -0
- package/src/modules/avnu.ts +4 -4
- package/src/modules/ekubo-pricer.ts +79 -0
- package/src/modules/ekubo-quoter.ts +46 -30
- package/src/modules/erc20.ts +17 -0
- package/src/modules/harvests.ts +43 -29
- package/src/modules/pragma.ts +23 -8
- package/src/modules/pricer-from-api.ts +156 -15
- package/src/modules/pricer-lst.ts +1 -1
- package/src/modules/pricer.ts +40 -4
- package/src/modules/pricerBase.ts +2 -1
- package/src/node/deployer.ts +36 -1
- package/src/node/pricer-redis.ts +2 -1
- package/src/strategies/base-strategy.ts +78 -10
- package/src/strategies/ekubo-cl-vault.tsx +906 -347
- package/src/strategies/factory.ts +159 -0
- package/src/strategies/index.ts +6 -1
- package/src/strategies/registry.ts +239 -0
- package/src/strategies/sensei.ts +335 -7
- package/src/strategies/svk-strategy.ts +97 -27
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
- package/src/strategies/universal-adapters/avnu-adapter.ts +177 -268
- package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
- package/src/strategies/universal-adapters/common-adapter.ts +206 -203
- package/src/strategies/universal-adapters/extended-adapter.ts +155 -336
- package/src/strategies/universal-adapters/index.ts +9 -8
- package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
- package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +110 -75
- package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
- package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +762 -844
- package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
- package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
- package/src/strategies/universal-lst-muliplier-strategy.tsx +396 -204
- package/src/strategies/universal-strategy.tsx +1426 -1178
- package/src/strategies/vesu-extended-strategy/services/executionService.ts +2251 -0
- package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +2941 -0
- package/src/strategies/vesu-extended-strategy/services/operationService.ts +12 -1
- package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +52 -0
- package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
- package/src/strategies/vesu-extended-strategy/utils/constants.ts +2 -0
- package/src/strategies/vesu-extended-strategy/utils/helper.ts +158 -124
- package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +377 -1788
- package/src/strategies/vesu-rebalance.tsx +255 -152
- package/src/utils/health-factor-math.ts +4 -1
- package/src/utils/index.ts +2 -1
- package/src/utils/logger.browser.ts +22 -4
- package/src/utils/logger.node.ts +259 -24
- package/src/utils/starknet-call-parser.ts +1036 -0
- package/src/utils/strategy-utils.ts +61 -0
- package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
package/src/strategies/sensei.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
|
|
1
|
+
import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, StrategyTag, TokenInfo, AuditStatus, SourceCodeType, AccessControlType, InstantWithdrawalVault, StrategyLiveStatus, VaultType } from "@/interfaces";
|
|
2
2
|
import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
|
|
3
3
|
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
4
|
-
import { Call, Contract, uint256 } from "starknet";
|
|
4
|
+
import { Call, Contract, num, uint256, BlockIdentifier } from "starknet";
|
|
5
5
|
import SenseiABI from "@/data/sensei.abi.json";
|
|
6
6
|
import { getTrovesEndpoint, logger } from "@/utils";
|
|
7
7
|
import { Global } from "@/global";
|
|
@@ -9,6 +9,10 @@ import { QuoteRequest } from "@avnu/avnu-sdk";
|
|
|
9
9
|
import { PricerBase } from "@/modules/pricerBase";
|
|
10
10
|
import ERC20ABI from "@/data/erc20.abi.json";
|
|
11
11
|
import { AvnuWrapper } from "@/modules";
|
|
12
|
+
import { gql } from "@apollo/client";
|
|
13
|
+
import apolloClient from "@/modules/apollo-client";
|
|
14
|
+
import { VesuAdapter, VesuPools } from "./universal-adapters/vesu-adapter";
|
|
15
|
+
import { LSTAPRService } from "@/modules/lst-apr";
|
|
12
16
|
|
|
13
17
|
export interface SenseiVaultSettings {
|
|
14
18
|
mainToken: TokenInfo;
|
|
@@ -37,17 +41,27 @@ export class SenseiVault extends BaseStrategy<
|
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
async getUserTVL(user: ContractAddr): Promise<SingleTokenInfo> {
|
|
44
|
+
async getUserTVL(user: ContractAddr, blockIdentifier: BlockIdentifier = "latest"): Promise<SingleTokenInfo> {
|
|
41
45
|
const result: any = await this.contract.call(
|
|
42
46
|
"describe_position",
|
|
43
47
|
[user.address],
|
|
48
|
+
{
|
|
49
|
+
blockIdentifier,
|
|
50
|
+
}
|
|
44
51
|
);
|
|
45
52
|
const amount = Web3Number.fromWei(
|
|
46
53
|
uint256.uint256ToBN(result[1].estimated_size).toString(),
|
|
47
54
|
this.metadata.depositTokens[0].decimals,
|
|
48
55
|
)
|
|
56
|
+
|
|
57
|
+
// Convert blockIdentifier to block number for pricer if it's a number
|
|
58
|
+
const blockNumber = typeof blockIdentifier === 'number' || typeof blockIdentifier === 'bigint'
|
|
59
|
+
? Number(blockIdentifier)
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
49
62
|
const price = await this.pricer.getPrice(
|
|
50
63
|
this.metadata.depositTokens[0].symbol,
|
|
64
|
+
blockNumber
|
|
51
65
|
);
|
|
52
66
|
return {
|
|
53
67
|
usdValue: Number(amount.toFixed(6)) * price.price,
|
|
@@ -223,6 +237,268 @@ export class SenseiVault extends BaseStrategy<
|
|
|
223
237
|
logger.verbose('getSettings', settings);
|
|
224
238
|
return settings;
|
|
225
239
|
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Calculate lifetime earnings for a user
|
|
243
|
+
* Not yet implemented for Sensei Vault strategy
|
|
244
|
+
*/
|
|
245
|
+
getLifetimeEarnings(
|
|
246
|
+
userTVL: SingleTokenInfo,
|
|
247
|
+
investmentFlows: Array<{ amount: string; type: string; timestamp: number; tx_hash: string }>
|
|
248
|
+
): any {
|
|
249
|
+
throw new Error("getLifetimeEarnings is not implemented yet for this strategy");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async netAPY(): Promise<number> {
|
|
253
|
+
try {
|
|
254
|
+
// Fetch Vesu pools and select the Re7 xSTRK pool
|
|
255
|
+
const { pools } = await VesuAdapter.getVesuPools();
|
|
256
|
+
const re7PoolId = VesuPools.Re7xSTRK;
|
|
257
|
+
const pool = pools.find((p: any) =>
|
|
258
|
+
ContractAddr.from(num.getHexString(p.id)).eq(re7PoolId),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!pool) {
|
|
262
|
+
logger.warn(`${SenseiVault.name}::netAPY - Re7 xSTRK pool not found`);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mainSymbol = this.metadata.additionalInfo.mainToken.symbol.toLowerCase(); // STRK
|
|
267
|
+
const secondarySymbol =
|
|
268
|
+
this.metadata.additionalInfo.secondaryToken.symbol.toLowerCase(); // xSTRK
|
|
269
|
+
|
|
270
|
+
const collateralAssetStats = pool.assets.find(
|
|
271
|
+
(a: any) => String(a.symbol).toLowerCase() === secondarySymbol,
|
|
272
|
+
)?.stats;
|
|
273
|
+
const debtAssetStats = pool.assets.find(
|
|
274
|
+
(a: any) => String(a.symbol).toLowerCase() === mainSymbol,
|
|
275
|
+
)?.stats;
|
|
276
|
+
|
|
277
|
+
if (!collateralAssetStats || !debtAssetStats) {
|
|
278
|
+
logger.warn(
|
|
279
|
+
`${SenseiVault.name}::netAPY - Missing collateral/debt stats on Vesu pool`,
|
|
280
|
+
);
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Base xSTRK lending APY from Vesu
|
|
285
|
+
const xstrkSupplyAPY =
|
|
286
|
+
Number(collateralAssetStats.supplyApy?.value || 0) / 1e18;
|
|
287
|
+
|
|
288
|
+
// STRK rewards APR on xSTRK collateral from Vesu
|
|
289
|
+
const strkRewardsAPR = collateralAssetStats.defiSpringSupplyApr
|
|
290
|
+
? Number(collateralAssetStats.defiSpringSupplyApr.value || 0) / 1e18
|
|
291
|
+
: 0;
|
|
292
|
+
|
|
293
|
+
// STRK borrow APY from Vesu
|
|
294
|
+
const borrowAPY =
|
|
295
|
+
Number(debtAssetStats.borrowApr?.value || 0) / 1e18;
|
|
296
|
+
|
|
297
|
+
// LST APR for xSTRK from Endur (based on underlying STRK asset)
|
|
298
|
+
const lstAPY = await LSTAPRService.getLSTAPR(
|
|
299
|
+
this.metadata.additionalInfo.mainToken.address,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Collateral APY = Vesu xSTRK supply + Endur xSTRK APR + STRK rewards
|
|
303
|
+
const collateralAPY = xstrkSupplyAPY + lstAPY + strkRewardsAPR;
|
|
304
|
+
|
|
305
|
+
const feeFactor = this.metadata.additionalInfo.feeBps / 10000; // convert bps to decimal
|
|
306
|
+
const feeAdjustedColAPY =
|
|
307
|
+
collateralAPY - strkRewardsAPR * feeFactor;
|
|
308
|
+
|
|
309
|
+
// Position info (collateral & debt in USD terms)
|
|
310
|
+
const { collateralUSDValue, debtUSDValue } =
|
|
311
|
+
await this.getPositionInfo();
|
|
312
|
+
|
|
313
|
+
const collateralUSD = Number(collateralUSDValue.toFixed(6));
|
|
314
|
+
const debtUSD = Number(debtUSDValue.toFixed(6));
|
|
315
|
+
|
|
316
|
+
// Compute expected leverage using the same math as app-side strategy
|
|
317
|
+
const targetHf = this.metadata.additionalInfo.targetHfBps / 10000;
|
|
318
|
+
const xSTRKPrice = await this.getSecondaryTokenPriceRelativeToMain();
|
|
319
|
+
const denominator = targetHf * xSTRKPrice - 0.87;
|
|
320
|
+
if (denominator <= 0) {
|
|
321
|
+
logger.warn(
|
|
322
|
+
`${SenseiVault.name}::netAPY - Invalid denominator in leverage calc`,
|
|
323
|
+
);
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
const borrowedSTRK = (0.87 * xSTRKPrice) / denominator;
|
|
327
|
+
const expectedLeverage = 1 + borrowedSTRK;
|
|
328
|
+
if (!Number.isFinite(expectedLeverage) || expectedLeverage <= 0) {
|
|
329
|
+
logger.warn(
|
|
330
|
+
`${SenseiVault.name}::netAPY - Non-positive or invalid expectedLeverage`,
|
|
331
|
+
);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const payoff =
|
|
336
|
+
collateralUSD * feeAdjustedColAPY - debtUSD * borrowAPY;
|
|
337
|
+
const investment = collateralUSD - debtUSD;
|
|
338
|
+
|
|
339
|
+
if (investment === 0) {
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const netAPY = payoff / investment;
|
|
344
|
+
return Number.isFinite(netAPY) ? netAPY : 0;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
logger.error(`${SenseiVault.name}::netAPY - Error`, error);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Calculates user realized APY based on position growth accounting for deposits and withdrawals.
|
|
353
|
+
* Returns the APY as a number.
|
|
354
|
+
* Not implemented for Sensei Strategy yet.
|
|
355
|
+
*/
|
|
356
|
+
async getUserRealizedAPY(
|
|
357
|
+
blockIdentifier: BlockIdentifier = "latest",
|
|
358
|
+
sinceBlocks = 600000
|
|
359
|
+
): Promise<number> {
|
|
360
|
+
throw new Error("getUserRealizedAPY not implemented yet for Sensei strategy");
|
|
361
|
+
|
|
362
|
+
/*
|
|
363
|
+
logger.verbose(
|
|
364
|
+
`${SenseiVault.name}: getUserRealizedAPY => starting with userAddress=${userAddress.address}, blockIdentifier=${blockIdentifier}, sinceBlocks=${sinceBlocks}`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Determine current block number
|
|
368
|
+
let blockNow =
|
|
369
|
+
typeof blockIdentifier === "number" || typeof blockIdentifier === "bigint"
|
|
370
|
+
? Number(blockIdentifier)
|
|
371
|
+
: (await this.config.provider.getBlockLatestAccepted()).block_number;
|
|
372
|
+
|
|
373
|
+
// Look back window, but never before launch block
|
|
374
|
+
const blockBefore = Math.max(
|
|
375
|
+
blockNow - sinceBlocks,
|
|
376
|
+
this.metadata.launchBlock
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
logger.verbose(
|
|
380
|
+
`${SenseiVault.name}: getUserRealizedAPY => blockNow=${blockNow}, blockBefore=${blockBefore}`
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Get current estimated size
|
|
384
|
+
const currentResult: any = await this.contract.call(
|
|
385
|
+
"describe_position",
|
|
386
|
+
[userAddress.address],
|
|
387
|
+
{
|
|
388
|
+
blockIdentifier,
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
const currentEstimatedSize = Web3Number.fromWei(
|
|
392
|
+
uint256.uint256ToBN(currentResult[1].estimated_size).toString(),
|
|
393
|
+
this.metadata.depositTokens[0].decimals,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Get previous estimated size
|
|
397
|
+
const previousResult: any = await this.contract.call(
|
|
398
|
+
"describe_position",
|
|
399
|
+
[userAddress.address],
|
|
400
|
+
{
|
|
401
|
+
blockIdentifier: blockBefore,
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
const previousEstimatedSize = Web3Number.fromWei(
|
|
405
|
+
uint256.uint256ToBN(previousResult[1].estimated_size).toString(),
|
|
406
|
+
this.metadata.depositTokens[0].decimals,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
logger.verbose(
|
|
410
|
+
`${SenseiVault.name}: getUserRealizedAPY => currentEstimatedSize=${currentEstimatedSize.toString()}, previousEstimatedSize=${previousEstimatedSize.toString()}`
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Query GraphQL for deposits and withdrawals between blockBefore and blockNow
|
|
414
|
+
let newDeposits = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
|
|
415
|
+
let newWithdrawals = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const { data } = await apolloClient.query({
|
|
419
|
+
query: gql`
|
|
420
|
+
query Query($where: Investment_flowsWhereInput) {
|
|
421
|
+
findManyInvestment_flows(where: $where) {
|
|
422
|
+
block_number
|
|
423
|
+
amount
|
|
424
|
+
type
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
`,
|
|
428
|
+
variables: {
|
|
429
|
+
where: {
|
|
430
|
+
contract: {
|
|
431
|
+
equals: this.address.address.toLowerCase(),
|
|
432
|
+
},
|
|
433
|
+
owner: {
|
|
434
|
+
equals: userAddress.address.toLowerCase(),
|
|
435
|
+
},
|
|
436
|
+
block_number: {
|
|
437
|
+
gte: blockBefore,
|
|
438
|
+
lte: blockNow,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
fetchPolicy: 'no-cache',
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Sum deposits and withdrawals
|
|
446
|
+
for (const flow of data.findManyInvestment_flows) {
|
|
447
|
+
const amount = Web3Number.fromWei(
|
|
448
|
+
flow.amount,
|
|
449
|
+
this.metadata.depositTokens[0].decimals
|
|
450
|
+
);
|
|
451
|
+
if (flow.type === 'deposit') {
|
|
452
|
+
newDeposits = newDeposits.plus(amount);
|
|
453
|
+
} else if (flow.type === 'withdraw') {
|
|
454
|
+
newWithdrawals = newWithdrawals.plus(amount);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
logger.verbose(
|
|
459
|
+
`${SenseiVault.name}: getUserRealizedAPY => newDeposits=${newDeposits.toString()}, newWithdrawals=${newWithdrawals.toString()}`
|
|
460
|
+
);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
logger.verbose(
|
|
463
|
+
`${SenseiVault.name}: getUserRealizedAPY => Error querying GraphQL, continuing with zero deposits/withdrawals:`,
|
|
464
|
+
error
|
|
465
|
+
);
|
|
466
|
+
// Continue with zero deposits/withdrawals if GraphQL query fails
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Calculate growth: Current estimated size - new deposits + new withdrawals - previous estimated size
|
|
470
|
+
const growth = currentEstimatedSize
|
|
471
|
+
.minus(newDeposits)
|
|
472
|
+
.plus(newWithdrawals)
|
|
473
|
+
.minus(previousEstimatedSize);
|
|
474
|
+
|
|
475
|
+
logger.verbose(
|
|
476
|
+
`${SenseiVault.name}: getUserRealizedAPY => growth=${growth.toString()}`
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Handle edge case where previous position is zero
|
|
480
|
+
if (previousEstimatedSize.isZero() || previousEstimatedSize.lte(0)) {
|
|
481
|
+
logger.verbose(
|
|
482
|
+
`${SenseiVault.name}: getUserRealizedAPY => Previous position is zero, returning 0`
|
|
483
|
+
);
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Calculate actual block difference (in case limited by launch block)
|
|
488
|
+
const actualBlockDiff = blockNow - blockBefore;
|
|
489
|
+
|
|
490
|
+
// Calculate APY: 100 * 365/n * growth / previousEstimatedSize
|
|
491
|
+
// where n is the number of blocks (sinceBlocks or actual block difference)
|
|
492
|
+
const growthRatio = growth.dividedBy(previousEstimatedSize);
|
|
493
|
+
const apy = Number(growthRatio) * 100 * 365 / actualBlockDiff;
|
|
494
|
+
|
|
495
|
+
logger.verbose(
|
|
496
|
+
`${SenseiVault.name}: getUserRealizedAPY => actualBlockDiff=${actualBlockDiff}, growthRatio=${Number(growthRatio)}, apy=${apy}`
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return apy;
|
|
500
|
+
*/
|
|
501
|
+
}
|
|
226
502
|
|
|
227
503
|
}
|
|
228
504
|
|
|
@@ -289,9 +565,10 @@ const FAQS = [
|
|
|
289
565
|
export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
|
|
290
566
|
[
|
|
291
567
|
{
|
|
568
|
+
id: "xstrk_sensei",
|
|
292
569
|
name: "xSTRK Sensei",
|
|
293
570
|
description: highlightTextWithLinks(
|
|
294
|
-
senseiDescription.
|
|
571
|
+
senseiDescription.replace('{{token1}}', 'STRK').replace('{{token2}}', 'xSTRK'),
|
|
295
572
|
[{
|
|
296
573
|
highlight: "Endur",
|
|
297
574
|
link: "https://endur.fi"
|
|
@@ -308,11 +585,37 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
|
|
|
308
585
|
),
|
|
309
586
|
launchBlock: 1053811,
|
|
310
587
|
type: "Other",
|
|
588
|
+
curator: {
|
|
589
|
+
name: "Unwrap Labs",
|
|
590
|
+
logo: "https://assets.troves.fi/integrations/unwraplabs/white.png"
|
|
591
|
+
},
|
|
592
|
+
vaultType: {
|
|
593
|
+
type: VaultType.LOOPING,
|
|
594
|
+
description: "Creates leveraged looping position on xSTRK by borrowing STRK to increase yield"
|
|
595
|
+
},
|
|
311
596
|
depositTokens: [
|
|
312
597
|
Global.getDefaultTokens().find((t) => t.symbol === "STRK")!
|
|
313
598
|
],
|
|
314
599
|
protocols: [endurProtocol, vesuProtocol],
|
|
315
|
-
|
|
600
|
+
settings: {
|
|
601
|
+
maxTVL: new Web3Number("1500000", 18),
|
|
602
|
+
alerts: [
|
|
603
|
+
{
|
|
604
|
+
type: "info",
|
|
605
|
+
text: "Depeg-risk: If xSTRK price on DEXes deviates from expected price, you may lose money or may have to wait for the price to recover.",
|
|
606
|
+
tab: "all"
|
|
607
|
+
}
|
|
608
|
+
],
|
|
609
|
+
liveStatus: StrategyLiveStatus.ACTIVE,
|
|
610
|
+
isPaused: false,
|
|
611
|
+
isInMaintenance: false,
|
|
612
|
+
isAudited: false,
|
|
613
|
+
isInstantWithdrawal: true,
|
|
614
|
+
isTransactionHistDisabled: true,
|
|
615
|
+
quoteToken: Global.getDefaultTokens().find(
|
|
616
|
+
(t) => t.symbol === "STRK"
|
|
617
|
+
)!
|
|
618
|
+
},
|
|
316
619
|
risk: {
|
|
317
620
|
riskFactor: _riskFactor,
|
|
318
621
|
netRisk:
|
|
@@ -335,6 +638,31 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
|
|
|
335
638
|
"Buy more xSTRK with borrowed STRK",
|
|
336
639
|
"Repeat the process to loop your position",
|
|
337
640
|
"Claim DeFi spring (STRK) rewards weekly and reinvest",
|
|
338
|
-
]
|
|
641
|
+
],
|
|
642
|
+
tags: [StrategyTag.LEVERED],
|
|
643
|
+
security: {
|
|
644
|
+
auditStatus: AuditStatus.AUDITED,
|
|
645
|
+
sourceCode: {
|
|
646
|
+
type: SourceCodeType.CLOSED_SOURCE,
|
|
647
|
+
contractLink: "https://github.com/trovesfi/troves-contracts",
|
|
648
|
+
},
|
|
649
|
+
accessControl: {
|
|
650
|
+
type: AccessControlType.STANDARD_ACCOUNT,
|
|
651
|
+
addresses: [ContractAddr.from("0x0")],
|
|
652
|
+
timeLock: "2 Days",
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
redemptionInfo: {
|
|
656
|
+
instantWithdrawalVault: InstantWithdrawalVault.YES,
|
|
657
|
+
redemptionsInfo: [],
|
|
658
|
+
alerts: [],
|
|
659
|
+
},
|
|
660
|
+
usualTimeToEarnings: "2 weeks",
|
|
661
|
+
usualTimeToEarningsDescription: "Strategy returns depend on LST price on DEXes. Even though the true price of LST on Endur increases continuously, the DEX price may lag sometimes, and historically is seen to rebase at least once every 2 hours. This is when you realise your earnings.",
|
|
662
|
+
points: [{
|
|
663
|
+
multiplier: 4,
|
|
664
|
+
logo: 'https://endur.fi/favicon.ico',
|
|
665
|
+
toolTip: "This strategy holds xSTRK. Earn 3-4x Endur points on your xSTRK due to the leverage. Points can be found on endur.fi.",
|
|
666
|
+
}]
|
|
339
667
|
},
|
|
340
|
-
];
|
|
668
|
+
];
|
|
@@ -3,7 +3,7 @@ import { IConfig, IStrategyMetadata, TokenInfo, VaultPosition } from "@/interfac
|
|
|
3
3
|
import { PricerBase } from "@/modules/pricerBase";
|
|
4
4
|
import { ERC20 } from "@/modules";
|
|
5
5
|
import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
|
|
6
|
-
import { Call, Contract, num } from "starknet";
|
|
6
|
+
import { Call, Contract, num, uint256 } from "starknet";
|
|
7
7
|
import { assert, LeafData, logger, StandardMerkleTree } from "@/utils";
|
|
8
8
|
import { UniversalStrategySettings } from "./universal-strategy";
|
|
9
9
|
import { UNIVERSAL_MANAGE_IDS } from "./universal-strategy";
|
|
@@ -15,9 +15,9 @@ import { ManageCall, PositionInfo } from "./universal-adapters";
|
|
|
15
15
|
* Base class for all SVK (Starknet Vault Kit) strategies.
|
|
16
16
|
* Contains common functions that are shared across all SVK strategies.
|
|
17
17
|
*/
|
|
18
|
-
export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
18
|
+
export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
19
19
|
extends BaseStrategy<SingleTokenInfo, SingleActionAmount> {
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
/** Contract address of the strategy */
|
|
22
22
|
readonly address: ContractAddr;
|
|
23
23
|
/** Pricer instance for token price calculations */
|
|
@@ -36,13 +36,13 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
36
36
|
this.pricer = pricer;
|
|
37
37
|
this.metadata = metadata;
|
|
38
38
|
this.address = metadata.address;
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
this.contract = new Contract({
|
|
41
41
|
abi: UniversalVaultAbi,
|
|
42
42
|
address: this.address.address,
|
|
43
43
|
providerOrAccount: this.config.provider
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
this.managerContract = new Contract({
|
|
47
47
|
abi: ManagerAbi,
|
|
48
48
|
address: this.metadata.additionalInfo.manager.address,
|
|
@@ -57,14 +57,51 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
57
57
|
return this.metadata.depositTokens[0];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr): Promise<Call[]> {
|
|
61
|
+
// Technically its not erc4626 abi, but we just need approve call
|
|
62
|
+
// so, its ok to use it
|
|
63
|
+
assert(
|
|
64
|
+
amountInfo.tokenInfo.address.eq(this.asset().address),
|
|
65
|
+
"Deposit token mismatch"
|
|
66
|
+
);
|
|
67
|
+
const assetContract = new Contract({
|
|
68
|
+
abi: UniversalVaultAbi,
|
|
69
|
+
address: this.asset().address.address,
|
|
70
|
+
providerOrAccount: this.config.provider
|
|
71
|
+
});
|
|
72
|
+
const call1 = assetContract.populate("approve", [
|
|
73
|
+
this.address.address,
|
|
74
|
+
uint256.bnToUint256(amountInfo.amount.toWei())
|
|
75
|
+
]);
|
|
76
|
+
const call2 = this.contract.populate("deposit", [
|
|
77
|
+
uint256.bnToUint256(amountInfo.amount.toWei()),
|
|
78
|
+
receiver.address
|
|
79
|
+
]);
|
|
80
|
+
return [call1, call2];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async withdrawCall(amountInfo: SingleActionAmount, receiver: ContractAddr, owner: ContractAddr): Promise<Call[]> {
|
|
84
|
+
assert(
|
|
85
|
+
amountInfo.tokenInfo.address.eq(this.asset().address),
|
|
86
|
+
"Withdraw token mismatch"
|
|
87
|
+
);
|
|
88
|
+
const shares = await this.contract.call('convert_to_shares', [uint256.bnToUint256(amountInfo.amount.toWei())]);
|
|
89
|
+
const call = this.contract.populate("request_redeem", [
|
|
90
|
+
uint256.bnToUint256(shares.toString()),
|
|
91
|
+
receiver.address,
|
|
92
|
+
owner.address
|
|
93
|
+
]);
|
|
94
|
+
return [call];
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
/**
|
|
61
98
|
* Returns the unused balance in the vault allocator.
|
|
62
99
|
* Note: This function is common for any SVK strategy.
|
|
63
100
|
*/
|
|
64
101
|
async getUnusedBalance(): Promise<SingleTokenInfo> {
|
|
65
102
|
const balance = await (new ERC20(this.config)).balanceOf(
|
|
66
|
-
this.asset().address,
|
|
67
|
-
this.metadata.additionalInfo.vaultAllocator,
|
|
103
|
+
this.asset().address,
|
|
104
|
+
this.metadata.additionalInfo.vaultAllocator,
|
|
68
105
|
this.asset().decimals
|
|
69
106
|
);
|
|
70
107
|
const price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
|
|
@@ -76,6 +113,23 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
76
113
|
};
|
|
77
114
|
}
|
|
78
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Builds a manage call from an adapter's proof tree.
|
|
118
|
+
* DRY helper for the repeated getProofs → getManageCall pattern.
|
|
119
|
+
*/
|
|
120
|
+
protected async buildManageCallFromAdapter(
|
|
121
|
+
adapter: { getProofs: (isDeposit: boolean, tree: any) => any },
|
|
122
|
+
isDeposit: boolean,
|
|
123
|
+
amount: Web3Number,
|
|
124
|
+
): Promise<Call> {
|
|
125
|
+
const proofsInfo = adapter.getProofs(isDeposit, this.getMerkleTree());
|
|
126
|
+
const manageCalls = await proofsInfo.callConstructor({ amount });
|
|
127
|
+
return this.getManageCall(
|
|
128
|
+
this.getProofGroupsForManageCalls(manageCalls),
|
|
129
|
+
manageCalls,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
/**
|
|
80
134
|
* Bridges liquidity from the vault allocator back to the vault.
|
|
81
135
|
* Note: This function is common for any SVK strategy.
|
|
@@ -85,37 +139,54 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
85
139
|
const approveAdapter = this.metadata.additionalInfo.leafAdapters.find(
|
|
86
140
|
adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY)
|
|
87
141
|
);
|
|
88
|
-
|
|
142
|
+
|
|
89
143
|
if (!approveAdapter) {
|
|
90
144
|
throw new Error(`${this.getTag()}::getBringLiquidityCall: approve adapter not found`);
|
|
91
145
|
}
|
|
92
|
-
|
|
146
|
+
|
|
93
147
|
// Find bring liquidity leaf adapter
|
|
94
148
|
const bringLiquidityAdapter = this.metadata.additionalInfo.leafAdapters.find(
|
|
95
149
|
adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY)
|
|
96
150
|
);
|
|
97
|
-
|
|
151
|
+
|
|
98
152
|
if (!bringLiquidityAdapter) {
|
|
99
153
|
throw new Error(`${this.getTag()}::getBringLiquidityCall: bring liquidity adapter not found`);
|
|
100
154
|
}
|
|
101
|
-
|
|
102
|
-
// Get proofs
|
|
103
|
-
const tree = this.getMerkleTree();
|
|
104
|
-
const proofGroups: string[][] = [];
|
|
105
|
-
for (const [i, v] of tree.entries()) {
|
|
106
|
-
if (v.readableId === UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY ||
|
|
107
|
-
v.readableId === UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY) {
|
|
108
|
-
proofGroups.push(tree.getProof(i));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
155
|
+
|
|
112
156
|
// Construct calls
|
|
113
157
|
logger.verbose("approve adapter amount", params.amount);
|
|
114
158
|
const approveCalls = await approveAdapter().callConstructor({ amount: params.amount });
|
|
115
159
|
const bringLiquidityCalls = await bringLiquidityAdapter().callConstructor({ amount: params.amount });
|
|
116
|
-
|
|
160
|
+
const manageCalls = [...approveCalls, ...bringLiquidityCalls];
|
|
161
|
+
|
|
117
162
|
// Combine into final call
|
|
118
|
-
return this.getManageCall(
|
|
163
|
+
return this.getManageCall(this.getProofGroupsForManageCalls(manageCalls), manageCalls);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves ordered merkle proofs from emitted manage calls.
|
|
168
|
+
* This ensures only runtime-required proofs are passed to manager calls.
|
|
169
|
+
*/
|
|
170
|
+
protected getProofGroupsForManageCalls(manageCalls: ManageCall[]): string[][] {
|
|
171
|
+
const tree = this.getMerkleTree();
|
|
172
|
+
const proofByReadableId = new Map<string, string[]>();
|
|
173
|
+
for (const [i, v] of tree.entries()) {
|
|
174
|
+
if (proofByReadableId.has(v.readableId)) {
|
|
175
|
+
throw new Error(`Duplicate readableId found in merkle tree: ${v.readableId}`);
|
|
176
|
+
}
|
|
177
|
+
proofByReadableId.set(v.readableId, tree.getProof(i));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return manageCalls.map((manageCall, index) => {
|
|
181
|
+
if (!manageCall.proofReadableId) {
|
|
182
|
+
throw new Error(`Missing proofReadableId for manage call at index ${index}`);
|
|
183
|
+
}
|
|
184
|
+
const proof = proofByReadableId.get(manageCall.proofReadableId);
|
|
185
|
+
if (!proof) {
|
|
186
|
+
throw new Error(`Proof not found for readableId: ${manageCall.proofReadableId}`);
|
|
187
|
+
}
|
|
188
|
+
return proof;
|
|
189
|
+
});
|
|
119
190
|
}
|
|
120
191
|
|
|
121
192
|
/**
|
|
@@ -157,10 +228,10 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
157
228
|
*/
|
|
158
229
|
getManageCall(proofGroups: string[][], manageCalls: ManageCall[]): Call {
|
|
159
230
|
assert(
|
|
160
|
-
proofGroups.length == manageCalls.length,
|
|
231
|
+
proofGroups.length == manageCalls.length,
|
|
161
232
|
`Proof IDs and Manage Calls length mismatch: ${proofGroups.length} != ${manageCalls.length}`
|
|
162
233
|
);
|
|
163
|
-
|
|
234
|
+
|
|
164
235
|
return this.managerContract.populate('manage_vault_with_merkle_verification', {
|
|
165
236
|
proofs: proofGroups.map(group => group),
|
|
166
237
|
decoder_and_sanitizers: manageCalls.map(call => call.sanitizer.address),
|
|
@@ -176,7 +247,7 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
176
247
|
*/
|
|
177
248
|
getSetManagerCall(strategist: ContractAddr, root = this.getMerkleRoot()): Call {
|
|
178
249
|
return this.managerContract.populate('set_manage_root', [
|
|
179
|
-
strategist.address,
|
|
250
|
+
strategist.address,
|
|
180
251
|
num.getHexString(root)
|
|
181
252
|
]);
|
|
182
253
|
}
|
|
@@ -244,4 +315,3 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
|
|
|
244
315
|
}
|
|
245
316
|
|
|
246
317
|
}
|
|
247
|
-
|
|
@@ -15,6 +15,7 @@ export const AVNU_EXCHANGE_FOR_LEGACY_USDC = ContractAddr.from('0x7bffc7f6bda62b
|
|
|
15
15
|
export const AVNU_QUOTE_URL = "https://starknet.api.avnu.fi/swap/v3/quotes";
|
|
16
16
|
export const EXTENDED_CONTRACT = ContractAddr.from('0x062da0780fae50d68cecaa5a051606dc21217ba290969b302db4dd99d2e9b470');
|
|
17
17
|
export const VESU_SINGLETON = ContractAddr.from('0x000d8d6dfec4d33bfb6895de9f3852143a17c6f92fd2a21da3d6924d34870160');
|
|
18
|
+
export const TRANSFER_SANITIZER = ContractAddr.from('0x07c94fd8762b37b02c2afeaf37e5d77523a2aedd370ccff6e130b2d5e9e23037');
|
|
18
19
|
export function toBigInt(value: string | number): bigint {
|
|
19
20
|
if (typeof value === 'string') {
|
|
20
21
|
return BigInt(value);
|
|
@@ -23,4 +24,4 @@ export function toBigInt(value: string | number): bigint {
|
|
|
23
24
|
} else {
|
|
24
25
|
throw new Error('Value must be a string or number');
|
|
25
26
|
}
|
|
26
|
-
}
|
|
27
|
+
}
|