@strkfarm/sdk 1.0.53 → 1.0.55

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.
@@ -61,6 +61,21 @@ export class _Web3Number<T extends _Web3Number<T>> extends BigNumber {
61
61
  }
62
62
  return value.toFixed(this.maxToFixedDecimals());
63
63
  }
64
+
65
+ minimum(value: string | number | T): T {
66
+ const _value = new BigNumber(value);
67
+ const _valueMe = new BigNumber(this.toString());
68
+ const answer = _value.lessThanOrEqualTo(_valueMe) ? _value : _valueMe;
69
+ return this.construct(answer.toString(), this.decimals);
70
+ }
71
+
72
+ maximum(value: string | number | T): T {
73
+ const _value = new BigNumber(value);
74
+ const _valueMe = new BigNumber(this.toString());
75
+ console.warn(`maximum: _value: ${_value.toString()}, _valueMe: ${_valueMe.toString()}`);
76
+ const answer = _value.greaterThanOrEqualTo(_valueMe) ? _value : _valueMe;
77
+ return this.construct(answer.toString(), this.decimals);
78
+ }
64
79
  }
65
80
 
66
81
  BigNumber.config({ DECIMAL_PLACES: 18, ROUNDING_MODE: BigNumber.ROUND_DOWN });
@@ -14,7 +14,8 @@ export enum RiskType {
14
14
  SMART_CONTRACT_RISK = "Smart Contract Risk",
15
15
  ORACLE_RISK = "Oracle Risk",
16
16
  TECHNICAL_RISK = "Technical Risk",
17
- COUNTERPARTY_RISK = "Counterparty Risk" // e.g. bad debt
17
+ COUNTERPARTY_RISK = "Counterparty Risk", // e.g. bad debt
18
+ DEPEG_RISK = "Depeg Risk" // e.g. USDC depeg
18
19
  }
19
20
 
20
21
  export interface RiskFactor {
@@ -104,7 +105,7 @@ export interface IInvestmentFlow {
104
105
  }
105
106
 
106
107
  export function getMainnetConfig(
107
- rpcUrl = "https://starknet-mainnet.public.blastapi.io",
108
+ rpcUrl: string,
108
109
  blockIdentifier: BlockIdentifier = "pending"
109
110
  ): IConfig {
110
111
  return {
@@ -135,6 +136,8 @@ export const getRiskExplaination = (riskType: RiskType) => {
135
136
  return "The risk of technical issues e.g. backend failure.";
136
137
  case RiskType.COUNTERPARTY_RISK:
137
138
  return "The risk of the counterparty defaulting e.g. bad debt on lending platforms.";
139
+ case RiskType.DEPEG_RISK:
140
+ return "The risk of a token losing its peg to the underlying asset, leading to potential losses for holders.";
138
141
  }
139
142
  };
140
143
 
@@ -60,7 +60,6 @@ export class AvnuWrapper {
60
60
  throw new Error('no quotes found')
61
61
  }
62
62
 
63
- logger.verbose(`${AvnuWrapper.name}: getQuotes => Found ${JSON.stringify(filteredQuotes[0])}`);
64
63
  return filteredQuotes[0];
65
64
  }
66
65
 
@@ -21,8 +21,14 @@ export interface DualTokenInfo {
21
21
  token1: SingleTokenInfo
22
22
  }
23
23
 
24
+ interface CacheData {
25
+ timestamp: number;
26
+ ttl: number;
27
+ data: any;
28
+ }
24
29
  export class BaseStrategy<TVLInfo, ActionInfo> {
25
30
  readonly config: IConfig;
31
+ readonly cache: Map<string, CacheData> = new Map();
26
32
 
27
33
  constructor(config: IConfig) {
28
34
  this.config = config;
@@ -43,5 +49,26 @@ export class BaseStrategy<TVLInfo, ActionInfo> {
43
49
  async withdrawCall(amountInfo: ActionInfo, receiver: ContractAddr, owner: ContractAddr): Promise<Call[]> {
44
50
  throw new Error("Not implemented");
45
51
  }
52
+
53
+ setCache(key: string, data: any, ttl: number = 60000): void {
54
+ const timestamp = Date.now();
55
+ this.cache.set(key, { timestamp, ttl, data });
56
+ }
57
+
58
+ getCache(key: string): any | null {
59
+ const cachedData = this.cache.get(key);
60
+ if (!cachedData || !this.isCacheValid(key)) {
61
+ return null;
62
+ }
63
+ return cachedData.data;
64
+ }
65
+
66
+ isCacheValid(key: string): boolean {
67
+ const cachedData = this.cache.get(key);
68
+ if (!cachedData) return false;
69
+
70
+ const { timestamp, ttl } = cachedData;
71
+ return Date.now() - timestamp <= ttl;
72
+ }
46
73
 
47
74
  }
@@ -1456,11 +1456,19 @@ export class EkuboCLVault extends BaseStrategy<
1456
1456
  const swap1Amount = Web3Number.fromWei(
1457
1457
  uint256.uint256ToBN(swapInfo1.token_from_amount).toString(),
1458
1458
  18 // cause its always STRK?
1459
- );
1459
+ ).minimum(
1460
+ postFeeAmount.toFixed(18) // cause always strk
1461
+ ); // ensure we don't swap more than we have
1462
+ swapInfo.token_from_amount = uint256.bnToUint256(swap1Amount.toWei());
1463
+ swapInfo.token_to_min_amount = uint256.bnToUint256(
1464
+ swap1Amount.multipliedBy(0).toWei() // placeholder
1465
+ ); // 0.01% slippage
1466
+
1460
1467
  logger.verbose(
1461
1468
  `${EkuboCLVault.name}: harvest => swap1Amount: ${swap1Amount}`
1462
1469
  );
1463
- const remainingAmount = postFeeAmount.minus(swap1Amount);
1470
+
1471
+ const remainingAmount = postFeeAmount.minus(swap1Amount).maximum(0);
1464
1472
  logger.verbose(
1465
1473
  `${EkuboCLVault.name}: harvest => remainingAmount: ${remainingAmount}`
1466
1474
  );
@@ -1500,7 +1508,11 @@ export class EkuboCLVault extends BaseStrategy<
1500
1508
  const _callsFinal = await this.rebalanceIter(
1501
1509
  swapInfo,
1502
1510
  acc,
1503
- harvestEstimateCall
1511
+ harvestEstimateCall,
1512
+ claim.token.eq(poolKey.token0),
1513
+ 0,
1514
+ 0n,
1515
+ BigInt(postFeeAmount.toWei()), // upper limit is the post fee amount
1504
1516
  );
1505
1517
  logger.verbose(
1506
1518
  `${EkuboCLVault.name}: harvest => _callsFinal: ${JSON.stringify(
@@ -2,3 +2,4 @@ export * from './autoCompounderStrk';
2
2
  export * from './vesu-rebalance';
3
3
  export * from './ekubo-cl-vault';
4
4
  export * from './base-strategy';
5
+ export * from './sensei';
@@ -0,0 +1,333 @@
1
+ import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
2
+ import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
3
+ import { ContractAddr, Web3Number } from "@/dataTypes";
4
+ import { Call, Contract, uint256 } from "starknet";
5
+ import SenseiABI from "@/data/sensei.abi.json";
6
+ import { getTrovesEndpoint, logger } from "@/utils";
7
+ import { Global } from "@/global";
8
+ import { QuoteRequest } from "@avnu/avnu-sdk";
9
+ import { PricerBase } from "@/modules/pricerBase";
10
+ import ERC20ABI from "@/data/erc20.abi.json";
11
+ import { AvnuWrapper } from "@/modules";
12
+
13
+ export interface SenseiVaultSettings {
14
+ mainToken: TokenInfo;
15
+ secondaryToken: TokenInfo;
16
+ targetHfBps: number; // in bps
17
+ feeBps: number; // in bps
18
+ }
19
+
20
+ export class SenseiVault extends BaseStrategy<
21
+ SingleTokenInfo,
22
+ SingleActionAmount
23
+ > {
24
+ readonly address: ContractAddr;
25
+ readonly metadata: IStrategyMetadata<SenseiVaultSettings>;
26
+ readonly pricer: PricerBase;
27
+ readonly contract: Contract;
28
+ constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<SenseiVaultSettings>) {
29
+ super(config);
30
+ this.address = metadata.address;
31
+ this.pricer = pricer;
32
+ this.metadata = metadata;
33
+ this.contract = new Contract(SenseiABI, this.address.address, this.config.provider);
34
+
35
+ if (metadata.depositTokens.length === 0) {
36
+ throw new Error("Deposit tokens are not defined in metadata");
37
+ }
38
+ }
39
+
40
+ async getUserTVL(user: ContractAddr): Promise<SingleTokenInfo> {
41
+ const result: any = await this.contract.call(
42
+ "describe_position",
43
+ [user.address],
44
+ );
45
+ const amount = Web3Number.fromWei(
46
+ uint256.uint256ToBN(result[1].estimated_size).toString(),
47
+ this.metadata.depositTokens[0].decimals,
48
+ )
49
+ const price = await this.pricer.getPrice(
50
+ this.metadata.depositTokens[0].symbol,
51
+ );
52
+ return {
53
+ usdValue: Number(amount.toFixed(6)) * price.price,
54
+ amount,
55
+ tokenInfo: this.metadata.depositTokens[0],
56
+ }
57
+ }
58
+
59
+ async getTVL(): Promise<SingleTokenInfo> {
60
+ try {
61
+ const {
62
+ collateralXSTRK,
63
+ collateralUSDValue,
64
+ debtSTRK,
65
+ debtUSDValue,
66
+ xSTRKPrice,
67
+ collateralInSTRK,
68
+ } = await this.getPositionInfo();
69
+
70
+ const usdValue =
71
+ Number(collateralUSDValue.toFixed(6)) -
72
+ Number(debtUSDValue.toFixed(6));
73
+
74
+ return {
75
+ usdValue,
76
+ amount: new Web3Number(
77
+ (collateralInSTRK - Number(debtSTRK.toFixed(6))).toFixed(6),
78
+ collateralXSTRK.decimals,
79
+ ),
80
+ tokenInfo: this.metadata.depositTokens[0],
81
+ };
82
+ } catch (error) {
83
+ console.error('Error fetching TVL:', error);
84
+ return {
85
+ usdValue: 0,
86
+ amount: new Web3Number('0', this.metadata.depositTokens[0].decimals),
87
+ tokenInfo: this.metadata.depositTokens[0],
88
+ }
89
+ }
90
+ }
91
+
92
+ async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr): Promise<Call[]> {
93
+ const mainTokenContract = new Contract(
94
+ ERC20ABI,
95
+ this.metadata.depositTokens[0].address.address,
96
+ this.config.provider,
97
+ );
98
+ const call1 = mainTokenContract.populate('approve', [
99
+ this.address.address,
100
+ uint256.bnToUint256(amountInfo.amount.toWei()),
101
+ ]);
102
+ const call2 = this.contract.populate('deposit', [
103
+ uint256.bnToUint256(amountInfo.amount.toWei()),
104
+ receiver.address,
105
+ ]);
106
+
107
+ const calls = [call1, call2];
108
+ return calls;
109
+ }
110
+
111
+ async withdrawCall(amountInfo: SingleActionAmount, receiver: ContractAddr, owner: ContractAddr): Promise<Call[]> {
112
+ const call = this.contract.populate('withdraw', [
113
+ uint256.bnToUint256(amountInfo.amount.toWei()),
114
+ receiver.address,
115
+ 300, // 3% max slippage
116
+ ]);
117
+ return [call];
118
+ }
119
+
120
+ async getPositionInfo(): Promise<{
121
+ collateralXSTRK: Web3Number;
122
+ collateralUSDValue: Web3Number;
123
+ debtSTRK: Web3Number;
124
+ debtUSDValue: Web3Number;
125
+ xSTRKPrice: number;
126
+ collateralInSTRK: number;
127
+ }> {
128
+ const CACHE_KEY = 'positionInfo';
129
+ if (this.isCacheValid(CACHE_KEY)) {
130
+ return this.getCache(CACHE_KEY);
131
+ }
132
+ const resp = await fetch(
133
+ `${getTrovesEndpoint()}/vesu/positions?walletAddress=${this.address.address}`,
134
+ );
135
+ const data = await resp.json();
136
+ if (!data.data || data.data.length == 0) {
137
+ throw new Error('No positions found');
138
+ }
139
+
140
+ const collateralXSTRK = Web3Number.fromWei(
141
+ data.data[0].collateral.value,
142
+ data.data[0].collateral.decimals,
143
+ );
144
+ const collateralUSDValue = Web3Number.fromWei(
145
+ data.data[0].collateral.usdPrice.value,
146
+ data.data[0].collateral.usdPrice.decimals,
147
+ );
148
+ const debtSTRK = Web3Number.fromWei(
149
+ data.data[0].debt.value,
150
+ data.data[0].debt.decimals,
151
+ );
152
+ const debtUSDValue = Web3Number.fromWei(
153
+ data.data[0].debt.usdPrice.value,
154
+ data.data[0].debt.usdPrice.decimals,
155
+ );
156
+
157
+ const xSTRKPrice = await this.getSecondaryTokenPriceRelativeToMain();
158
+ const collateralInSTRK =
159
+ Number(collateralXSTRK.toFixed(6)) * xSTRKPrice;
160
+ const STRKUSDPrice =
161
+ Number(debtUSDValue.toFixed(6)) /
162
+ Number(debtSTRK.toFixed(6));
163
+ const actualCollateralUSDValue = collateralInSTRK * STRKUSDPrice;
164
+
165
+ const cacheData = {
166
+ collateralXSTRK,
167
+ collateralUSDValue: new Web3Number(
168
+ actualCollateralUSDValue.toFixed(6),
169
+ collateralUSDValue.decimals,
170
+ ),
171
+ debtSTRK,
172
+ debtUSDValue,
173
+ xSTRKPrice,
174
+ collateralInSTRK,
175
+ };
176
+ this.setCache(CACHE_KEY, cacheData); // cache for 1 hour
177
+ return cacheData;
178
+ }
179
+
180
+ async getSecondaryTokenPriceRelativeToMain(retry = 0): Promise<number> {
181
+ const CACHE_KEY = 'xSTRKPrice';
182
+ if (this.isCacheValid(CACHE_KEY)) {
183
+ return this.getCache(CACHE_KEY);
184
+ }
185
+ const params: QuoteRequest = {
186
+ sellTokenAddress: this.metadata.additionalInfo.secondaryToken.address.address,
187
+ buyTokenAddress: this.metadata.additionalInfo.mainToken.address.address,
188
+ sellAmount: BigInt(new Web3Number('1', 18).toWei()),
189
+ takerAddress: this.address.address,
190
+ };
191
+ logger.verbose('getSecondaryTokenPriceRelativeToMain [1]', params);
192
+ let avnu = new AvnuWrapper();
193
+ const quote = await avnu.getQuotes(
194
+ params.sellTokenAddress,
195
+ params.buyTokenAddress,
196
+ params.sellAmount?.toString() || '0',
197
+ params.takerAddress!
198
+ );
199
+ if (!quote) {
200
+ throw new Error('No quotes found to compute secondary token price relative to main token');
201
+ }
202
+
203
+ const firstQuote = quote;
204
+ const price = Number(
205
+ Web3Number.fromWei(firstQuote.buyAmount.toString(), 18).toFixed(
206
+ 6,
207
+ ),
208
+ );
209
+ logger.verbose('getSecondaryTokenPriceRelativeToMain [2]', price);
210
+ this.setCache(CACHE_KEY, price); // cache for 1 min
211
+ return price;
212
+ }
213
+
214
+ getSettings = async () => {
215
+ const settings = await this.contract.call('get_settings', []);
216
+ logger.verbose('getSettings', settings);
217
+ return settings;
218
+ };
219
+
220
+ }
221
+
222
+ const senseiDescription = `Deposit your {{token1}} to automatically loop your funds via Endur ({{token2}}) and Vesu to create a delta neutral position. This strategy is designed to maximize your yield on {{token1}}. Your position is automatically adjusted periodically to maintain a healthy health factor. You receive a NFT as representation for your stake on Troves. You can withdraw anytime by redeeming your NFT for {{token1}}.`;
223
+
224
+ const vesuProtocol: IProtocol = {
225
+ name: "Vesu",
226
+ logo: "https://static-assets-8zct.onrender.com/integrations/vesu/logo.png"
227
+ };
228
+ const endurProtocol: IProtocol = {
229
+ name: "Endur",
230
+ logo: "https://app.endur.fi/logo.png"
231
+ };
232
+ const _riskFactor: RiskFactor[] = [
233
+ { type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25, reason: "Audited by CSC" },
234
+ { type: RiskType.DEPEG_RISK, value: 0.25, weight: 25, reason: "Depending on prevailing market conditions and trading activity, xSTRK may lose its peg to the underlying asset." },
235
+ { type: RiskType.LIQUIDATION_RISK, value: 0.1, weight: 10, reason: "Liquidation risk is low due to the nature of the Re7 Pool on Vesu" },
236
+ { type: RiskType.LOW_LIQUIDITY_RISK, value: 0.5, weight: 50, reason: "xSTRK can be sometimes illiquid near true price" }
237
+ ];
238
+
239
+ const FAQS = [
240
+ {
241
+ question: "What is xSTRK Sensei?",
242
+ answer: "xSTRK Sensei is a leveraged looping strategy involving xSTRK and STRK. It uses xSTRK as collateral on Vesu, borrows STRK, and buys more xSTRK with it to create up to 4x leverage and boost yields."
243
+ },
244
+ {
245
+ question: "What is the benefit of using xSTRK Sensei?",
246
+ answer: "The strategy amplifies your xSTRK exposure and yield through leverage. It also helps you accumulate more Endur points faster."
247
+ },
248
+ {
249
+ question: "What is the maximum leverage possible?",
250
+ answer: "The strategy may allow up to ~4x leverage, depending on your collateral ratio and market conditions on Vesu. This strategy tries to maintain a health factor of 1.1 on Vesu",
251
+ },
252
+ {
253
+ question: "Isn't 1.1 health factor risky?",
254
+ answer: "Based on Re7's xSTRK pool configuration on Vesu, xSTRK uses STRK price as its oracle source. This means collateral and debt will always move in the same direction, making 1.1 HF safe. However, if debt increases too much (over months), liquidation may occur, which we try to avoid by actively monitoring the position."
255
+ },
256
+ {
257
+ question: "Are there any risks involved?",
258
+ answer: "Yes. The major risks are related to xSTRK's illiquidity and price volatility. During volatility or low liquidity, exiting a position can result in loss."
259
+ },
260
+ {
261
+ question: "Does the position always grow?",
262
+ answer: "No. While xSTRK's true value increases over time, its DEX price may not grow continuously and can fluctuate or move in discrete steps."
263
+ },
264
+ {
265
+ question: "Can I lose money using this strategy?",
266
+ answer: "Yes. If the xSTRK price drops sharply or becomes illiquid, you may face slippage or loss when trying to exit the looped position."
267
+ },
268
+ {
269
+ question: "What affects the DEX price of xSTRK?",
270
+ answer: "xSTRK's DEX price depends on supply, demand, and liquidity. Unlike its true value which grows steadily, the DEX price can fluctuate due to market activity."
271
+ },
272
+ {
273
+ question: "Why is xSTRK considered illiquid?",
274
+ answer: "Since xSTRK is a evolving LST with limited trading volume, sudden large trades can cause high slippage, making it harder to enter or exit positions efficiently. Such conditions normalize over time. Enter and exit positions with caution.",
275
+ },
276
+ {
277
+ question: "Do I earn Endur points on looped xSTRK?",
278
+ answer: "Yes. All xSTRK in the looped position contributes to your Endur points, allowing you to farm points more effectively with leverage. Visit endur.fi/leaderboard to see your points.",
279
+ }
280
+ ];
281
+
282
+ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
283
+ [
284
+ {
285
+ name: "xSTRK Sensei",
286
+ description: highlightTextWithLinks(
287
+ senseiDescription.replaceAll('{{token1}}', 'STRK').replaceAll('{{token2}}', 'xSTRK'),
288
+ [{
289
+ highlight: "Endur",
290
+ link: "https://endur.fi"
291
+ }, {
292
+ highlight: "Vesu",
293
+ link: "https://vesu.xyz"
294
+ }, {
295
+ highlight: 'delta neutral position',
296
+ link: 'https://www.investopedia.com/terms/d/deltaneutral.asp'
297
+ }]
298
+ ),
299
+ address: ContractAddr.from(
300
+ "0x7023a5cadc8a5db80e4f0fde6b330cbd3c17bbbf9cb145cbabd7bd5e6fb7b0b"
301
+ ),
302
+ launchBlock: 1053811,
303
+ type: "Other",
304
+ depositTokens: [
305
+ Global.getDefaultTokens().find((t) => t.symbol === "STRK")!
306
+ ],
307
+ protocols: [endurProtocol, vesuProtocol],
308
+ maxTVL: new Web3Number("1500000", 18),
309
+ risk: {
310
+ riskFactor: _riskFactor,
311
+ netRisk:
312
+ _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
313
+ _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
314
+ notARisks: getNoRiskTags(_riskFactor)
315
+ },
316
+ additionalInfo: {
317
+ mainToken: Global.getDefaultTokens().find((t) => t.symbol === "STRK")!,
318
+ secondaryToken: Global.getDefaultTokens().find((t) => t.symbol === "xSTRK")!,
319
+ targetHfBps: 11000, // 1.1 health factor
320
+ feeBps: 2000, // 2% fee on profits
321
+ },
322
+ faqs: FAQS,
323
+ contractDetails: [],
324
+ investmentSteps: [
325
+ "Swap STRK for xSTRK",
326
+ "Deposit xSTRK to Vesu's Re7 xSTRK Pool",
327
+ "Borrow STRK against your xSTRK collateral",
328
+ "Buy more xSTRK with borrowed STRK",
329
+ "Repeat the process to loop your position",
330
+ "Claim DeFi spring (STRK) rewards weekly and reinvest",
331
+ ]
332
+ },
333
+ ];
@@ -14,4 +14,8 @@ export function assert(condition: boolean, message: string) {
14
14
  if (!condition) {
15
15
  throw new Error(message);
16
16
  }
17
+ }
18
+
19
+ export function getTrovesEndpoint(): string {
20
+ return process.env.TROVES_ENDPOINT || 'https://app.troves.fi';
17
21
  }