@strkfarm/sdk 2.0.0-staging.4 → 2.0.0-staging.41
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 +10 -6
- package/dist/cli.mjs +10 -6
- package/dist/index.browser.global.js +29277 -26655
- package/dist/index.browser.mjs +4037 -1224
- package/dist/index.d.ts +287 -45
- package/dist/index.js +4316 -1489
- package/dist/index.mjs +4256 -1437
- package/package.json +4 -4
- package/src/data/yoloVault.abi.json +1109 -0
- package/src/dataTypes/_bignumber.ts +5 -0
- package/src/dataTypes/bignumber.browser.ts +5 -0
- package/src/dataTypes/bignumber.node.ts +5 -0
- package/src/global.ts +27 -0
- package/src/interfaces/common.tsx +68 -25
- package/src/modules/avnu.ts +1 -1
- package/src/modules/erc20.ts +18 -2
- package/src/strategies/base-strategy.ts +216 -6
- package/src/strategies/constants.ts +2 -2
- package/src/strategies/ekubo-cl-vault.tsx +210 -105
- package/src/strategies/factory.ts +21 -1
- package/src/strategies/index.ts +2 -0
- package/src/strategies/registry.ts +15 -30
- package/src/strategies/sensei.ts +156 -11
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +48 -27
- package/src/strategies/universal-lst-muliplier-strategy.tsx +1473 -574
- package/src/strategies/universal-strategy.tsx +141 -69
- package/src/strategies/vesu-rebalance.tsx +27 -11
- package/src/strategies/yoloVault.ts +747 -0
- package/src/utils/logger.node.ts +11 -4
- package/src/utils/strategy-utils.ts +6 -2
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessControlType,
|
|
3
|
+
AuditStatus,
|
|
4
|
+
getNoRiskTags,
|
|
5
|
+
highlightTextWithLinks,
|
|
6
|
+
IConfig,
|
|
7
|
+
InstantWithdrawalVault,
|
|
8
|
+
IStrategyMetadata,
|
|
9
|
+
RiskFactor,
|
|
10
|
+
SourceCodeType,
|
|
11
|
+
StrategyLiveStatus,
|
|
12
|
+
TokenInfo,
|
|
13
|
+
UnwrapLabsCurator,
|
|
14
|
+
VaultPosition,
|
|
15
|
+
VaultType,
|
|
16
|
+
} from "@/interfaces";
|
|
17
|
+
import { logger } from "@/utils";
|
|
18
|
+
import {
|
|
19
|
+
NetAPYDetails,
|
|
20
|
+
SingleActionAmount,
|
|
21
|
+
UserPositionCard,
|
|
22
|
+
UserPositionCardsInput,
|
|
23
|
+
} from "./base-strategy";
|
|
24
|
+
import {
|
|
25
|
+
BaseStrategy,
|
|
26
|
+
DualTokenInfo,
|
|
27
|
+
} from "./base-strategy";
|
|
28
|
+
import { uint256 } from "starknet";
|
|
29
|
+
import { ContractAddr } from "@/dataTypes";
|
|
30
|
+
import { Web3Number } from "@/dataTypes";
|
|
31
|
+
import { DualActionAmount } from "./base-strategy";
|
|
32
|
+
import { PricerBase } from "@/modules/pricerBase";
|
|
33
|
+
import { Call, Contract } from "starknet";
|
|
34
|
+
import YoloVaultAbi from "@/data/yoloVault.abi.json";
|
|
35
|
+
import { Global } from "@/global";
|
|
36
|
+
import { ERC20 } from "@/modules";
|
|
37
|
+
import { MY_ACCESS_CONTROL } from "./constants";
|
|
38
|
+
import { createElement } from "react";
|
|
39
|
+
|
|
40
|
+
export interface YoloVaultSettings {
|
|
41
|
+
startDate: string;
|
|
42
|
+
expiryDate: string;
|
|
43
|
+
mainToken: TokenInfo;
|
|
44
|
+
secondaryToken: TokenInfo;
|
|
45
|
+
totalEpochs: number;
|
|
46
|
+
minEpochDurationSeconds: number;
|
|
47
|
+
spendingLevels: YoloSpendingLevel[];
|
|
48
|
+
feeBps: number; // in bps
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface YoloSpendingLevel {
|
|
52
|
+
minPrice?: number;
|
|
53
|
+
maxPrice?: number;
|
|
54
|
+
spendPercent: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface YoloVaultStrategyConfig extends YoloVaultSettings {
|
|
58
|
+
id: string;
|
|
59
|
+
address: ContractAddr;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UserYoloInfo {
|
|
63
|
+
shares: bigint;
|
|
64
|
+
claimable_second_tokens: bigint;
|
|
65
|
+
base_token_balance: bigint;
|
|
66
|
+
base_token_consumed: bigint;
|
|
67
|
+
base_consumed_last_index: bigint;
|
|
68
|
+
second_token_last_index: bigint;
|
|
69
|
+
second_token_balance: bigint;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface YoloVaultStatus {
|
|
73
|
+
current_epoch: bigint;
|
|
74
|
+
total_epochs: bigint;
|
|
75
|
+
remaining_base: bigint;
|
|
76
|
+
total_second_tokens: bigint;
|
|
77
|
+
global_second_token_index: bigint;
|
|
78
|
+
cumulative_spend_index: bigint;
|
|
79
|
+
total_shares: bigint;
|
|
80
|
+
base_token_assets_per_share: bigint;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface YoloSettings {
|
|
84
|
+
base_token: bigint;
|
|
85
|
+
second_token: bigint;
|
|
86
|
+
total_epochs: bigint;
|
|
87
|
+
min_time_per_epoch: bigint;
|
|
88
|
+
max_spend_units_per_epoch: bigint;
|
|
89
|
+
base_token_assets_per_share: bigint;
|
|
90
|
+
oracle: bigint;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class YoLoVault extends BaseStrategy<DualTokenInfo, SingleActionAmount, DualActionAmount> {
|
|
94
|
+
readonly address: ContractAddr;
|
|
95
|
+
readonly metadata: IStrategyMetadata<YoloVaultSettings>;
|
|
96
|
+
readonly pricer: PricerBase;
|
|
97
|
+
readonly contract: Contract;
|
|
98
|
+
readonly primaryToken : TokenInfo;
|
|
99
|
+
readonly secondaryToken : TokenInfo;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
config: IConfig,
|
|
103
|
+
pricer: PricerBase,
|
|
104
|
+
metadata: IStrategyMetadata<YoloVaultSettings>,
|
|
105
|
+
) {
|
|
106
|
+
super(config, {
|
|
107
|
+
depositInputMode: "single",
|
|
108
|
+
withdrawInputMode: "dual",
|
|
109
|
+
});
|
|
110
|
+
this.address = metadata.address;
|
|
111
|
+
this.pricer = pricer;
|
|
112
|
+
this.metadata = metadata;
|
|
113
|
+
this.contract = new Contract({
|
|
114
|
+
abi: YoloVaultAbi,
|
|
115
|
+
address: this.address.address,
|
|
116
|
+
providerOrAccount: this.config.provider,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (metadata.depositTokens.length < 2) {
|
|
120
|
+
throw new Error("Deposit tokens are not fully defined in metadata");
|
|
121
|
+
}
|
|
122
|
+
this.primaryToken = metadata.depositTokens[0];
|
|
123
|
+
this.secondaryToken = metadata.depositTokens[1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// formatTokenAmount(amount: Web3Number, decimals: number): Web3Number {
|
|
128
|
+
// const formattedAmount = amount.dividedBy(10 ** decimals);
|
|
129
|
+
// return formattedAmount;
|
|
130
|
+
// }
|
|
131
|
+
|
|
132
|
+
private async getNormalizedUserInfo(user: ContractAddr): Promise<{
|
|
133
|
+
shares: Web3Number;
|
|
134
|
+
primaryTokenBalance: Web3Number;
|
|
135
|
+
secondaryTokenBalance: Web3Number;
|
|
136
|
+
claimableSecondaryTokens: Web3Number;
|
|
137
|
+
}> {
|
|
138
|
+
const userInfo = await this.contract.call("get_user_info", [user.address]);
|
|
139
|
+
const {
|
|
140
|
+
shares,
|
|
141
|
+
base_token_balance,
|
|
142
|
+
second_token_balance,
|
|
143
|
+
claimable_second_tokens,
|
|
144
|
+
} = userInfo as UserYoloInfo;
|
|
145
|
+
const userShares = new Web3Number(shares.toString(), 0);
|
|
146
|
+
const baseTokenBalance = Web3Number.fromWei(base_token_balance.toString(), this.primaryToken.decimals);
|
|
147
|
+
const secondTokenBalance = Web3Number.fromWei(second_token_balance.toString(), this.secondaryToken.decimals);
|
|
148
|
+
const claimableSecondTokens = Web3Number.fromWei(claimable_second_tokens.toString(), this.secondaryToken.decimals);
|
|
149
|
+
return {
|
|
150
|
+
shares: userShares,
|
|
151
|
+
primaryTokenBalance: baseTokenBalance,
|
|
152
|
+
secondaryTokenBalance: secondTokenBalance,
|
|
153
|
+
claimableSecondaryTokens: claimableSecondTokens,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private resolveWithdrawRequest(
|
|
158
|
+
amountInfo: DualActionAmount,
|
|
159
|
+
redeemableBaseTokenAmount: Web3Number,
|
|
160
|
+
redeemableSecondaryTokenAmount: Web3Number,
|
|
161
|
+
):
|
|
162
|
+
| {
|
|
163
|
+
sharesUsedFactor: number;
|
|
164
|
+
baseTokenAmountToWithdraw: number;
|
|
165
|
+
secondaryTokenAmountToWithdraw: number;
|
|
166
|
+
}
|
|
167
|
+
| null {
|
|
168
|
+
const baseTokenAmountToWithdraw = Number(amountInfo.token0.amount.toWei());
|
|
169
|
+
const secondaryTokenAmountToWithdraw = Number(amountInfo.token1.amount.toWei());
|
|
170
|
+
// if (baseTokenAmountToWithdraw > 0 && secondaryTokenAmountToWithdraw > 0) {
|
|
171
|
+
// throw new Error("Cannot pass amounts for both base and secondary tokens at once");
|
|
172
|
+
// }
|
|
173
|
+
if (
|
|
174
|
+
baseTokenAmountToWithdraw > 0 &&
|
|
175
|
+
redeemableBaseTokenAmount.greaterThanOrEqualTo(baseTokenAmountToWithdraw)
|
|
176
|
+
) {
|
|
177
|
+
const sharesUsedFactor = new Web3Number(baseTokenAmountToWithdraw.toString(), 0).dividedBy(redeemableBaseTokenAmount);
|
|
178
|
+
return {
|
|
179
|
+
sharesUsedFactor: sharesUsedFactor.toNumber(),
|
|
180
|
+
baseTokenAmountToWithdraw,
|
|
181
|
+
secondaryTokenAmountToWithdraw,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
secondaryTokenAmountToWithdraw > 0 &&
|
|
187
|
+
redeemableSecondaryTokenAmount.greaterThanOrEqualTo(
|
|
188
|
+
secondaryTokenAmountToWithdraw,
|
|
189
|
+
)
|
|
190
|
+
) {
|
|
191
|
+
const sharesUsedFactor = new Web3Number(secondaryTokenAmountToWithdraw.toString(), 0).dividedBy(redeemableSecondaryTokenAmount);
|
|
192
|
+
return {
|
|
193
|
+
sharesUsedFactor: sharesUsedFactor.toNumber(),
|
|
194
|
+
baseTokenAmountToWithdraw,
|
|
195
|
+
secondaryTokenAmountToWithdraw,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getUserTVL(user: ContractAddr): Promise<DualTokenInfo> {
|
|
203
|
+
try {
|
|
204
|
+
const [{ primaryTokenBalance, claimableSecondaryTokens }, primaryTokenPrice, secondaryTokenPrice] = await Promise.all([
|
|
205
|
+
this.getNormalizedUserInfo(user),
|
|
206
|
+
this.pricer.getPrice(this.primaryToken.symbol),
|
|
207
|
+
this.pricer.getPrice(this.secondaryToken.symbol),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
// ! todo u can simply do primaryTokenAmount.multipliedBy(primaryTokenPrice.price).toNumber()
|
|
211
|
+
// leaving the other unchanged to help u see the diff
|
|
212
|
+
const primaryTokenUsd = primaryTokenBalance.multipliedBy(primaryTokenPrice.price).toNumber();
|
|
213
|
+
const secondaryTokenUsd =claimableSecondaryTokens.multipliedBy(secondaryTokenPrice.price);
|
|
214
|
+
return {
|
|
215
|
+
usdValue: primaryTokenUsd + secondaryTokenUsd.toNumber(),
|
|
216
|
+
token0: {
|
|
217
|
+
tokenInfo: this.primaryToken,
|
|
218
|
+
amount: primaryTokenBalance,
|
|
219
|
+
usdValue: primaryTokenUsd,
|
|
220
|
+
},
|
|
221
|
+
token1: {
|
|
222
|
+
tokenInfo: this.secondaryToken,
|
|
223
|
+
amount: claimableSecondaryTokens,
|
|
224
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error("Error fetching user TVL:", error);
|
|
229
|
+
return {
|
|
230
|
+
usdValue: 0,
|
|
231
|
+
token0: {
|
|
232
|
+
tokenInfo: this.primaryToken,
|
|
233
|
+
amount: new Web3Number("0", this.primaryToken.decimals),
|
|
234
|
+
usdValue: 0,
|
|
235
|
+
},
|
|
236
|
+
token1: {
|
|
237
|
+
tokenInfo: this.secondaryToken,
|
|
238
|
+
amount: new Web3Number("0", this.secondaryToken.decimals),
|
|
239
|
+
usdValue: 0,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async getVaultPositions(): Promise<VaultPosition[]> {
|
|
246
|
+
const vaultStatus = await this.getVaultStatus();
|
|
247
|
+
const {
|
|
248
|
+
remaining_base: remainingBase,
|
|
249
|
+
total_second_tokens: totalSecondTokens,
|
|
250
|
+
} = vaultStatus as YoloVaultStatus;
|
|
251
|
+
const primaryTokenAmount = Web3Number.fromWei(remainingBase.toString(), this.primaryToken.decimals);
|
|
252
|
+
const secondaryTokenAmount = Web3Number.fromWei(totalSecondTokens.toString(), this.secondaryToken.decimals);
|
|
253
|
+
const [primaryTokenPrice, secondaryTokenPrice] = await Promise.all([
|
|
254
|
+
this.pricer.getPrice(this.primaryToken.symbol),
|
|
255
|
+
this.pricer.getPrice(this.secondaryToken.symbol),
|
|
256
|
+
]);
|
|
257
|
+
const primaryTokenUsd = primaryTokenAmount.multipliedBy(primaryTokenPrice.price);
|
|
258
|
+
const secondaryTokenUsd = secondaryTokenAmount.multipliedBy(secondaryTokenPrice.price);
|
|
259
|
+
return [{
|
|
260
|
+
amount: primaryTokenAmount,
|
|
261
|
+
usdValue: primaryTokenUsd.toNumber(),
|
|
262
|
+
token: this.primaryToken,
|
|
263
|
+
remarks: "Remaining deposit tokens in the Vault",
|
|
264
|
+
}, {
|
|
265
|
+
amount: secondaryTokenAmount,
|
|
266
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
267
|
+
token: this.secondaryToken,
|
|
268
|
+
remarks: "Total swapped tokens in the Vault",
|
|
269
|
+
}]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getTVL(): Promise<DualTokenInfo> {
|
|
273
|
+
try {
|
|
274
|
+
const [vaultStatus, primaryTokenPrice, secondaryTokenPrice] =
|
|
275
|
+
await Promise.all([
|
|
276
|
+
this.getVaultStatus(),
|
|
277
|
+
this.pricer.getPrice(this.primaryToken.symbol),
|
|
278
|
+
this.pricer.getPrice(this.secondaryToken.symbol),
|
|
279
|
+
]);
|
|
280
|
+
const {
|
|
281
|
+
remaining_base: remainingBase,
|
|
282
|
+
total_second_tokens: totalSecondTokens,
|
|
283
|
+
} = vaultStatus as YoloVaultStatus;
|
|
284
|
+
|
|
285
|
+
let primaryTokenAmount = Web3Number.fromWei(remainingBase.toString(), this.primaryToken.decimals);
|
|
286
|
+
let secondaryTokenAmount = Web3Number.fromWei(totalSecondTokens.toString(), this.secondaryToken.decimals);
|
|
287
|
+
const primaryTokenUsd = new Web3Number(
|
|
288
|
+
primaryTokenAmount.toFixed(this.primaryToken.decimals),
|
|
289
|
+
this.primaryToken.decimals,
|
|
290
|
+
).multipliedBy(primaryTokenPrice.price);
|
|
291
|
+
const secondaryTokenUsd = new Web3Number(
|
|
292
|
+
secondaryTokenAmount.toFixed(this.secondaryToken.decimals),
|
|
293
|
+
this.secondaryToken.decimals,
|
|
294
|
+
).multipliedBy(secondaryTokenPrice.price);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
usdValue: primaryTokenUsd.plus(secondaryTokenUsd).toNumber(),
|
|
298
|
+
token0: {
|
|
299
|
+
tokenInfo: this.primaryToken,
|
|
300
|
+
amount: primaryTokenAmount,
|
|
301
|
+
usdValue: primaryTokenUsd.toNumber(),
|
|
302
|
+
},
|
|
303
|
+
token1: {
|
|
304
|
+
tokenInfo: this.secondaryToken,
|
|
305
|
+
amount: secondaryTokenAmount,
|
|
306
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error("Error fetching vault TVL:", error);
|
|
311
|
+
return {
|
|
312
|
+
usdValue: 0,
|
|
313
|
+
token0: {
|
|
314
|
+
tokenInfo: this.primaryToken,
|
|
315
|
+
amount: new Web3Number("0", this.primaryToken.decimals),
|
|
316
|
+
usdValue: 0,
|
|
317
|
+
},
|
|
318
|
+
token1: {
|
|
319
|
+
tokenInfo: this.secondaryToken,
|
|
320
|
+
amount: new Web3Number("0", this.secondaryToken.decimals),
|
|
321
|
+
usdValue: 0,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async depositCall(
|
|
328
|
+
amountInfo: SingleActionAmount,
|
|
329
|
+
receiver: ContractAddr,
|
|
330
|
+
): Promise<Call[]> {
|
|
331
|
+
try{
|
|
332
|
+
const primaryToken = amountInfo.tokenInfo;
|
|
333
|
+
const approvalCall = new ERC20(this.config).approve(primaryToken.address.address, this.address.address, amountInfo.amount);
|
|
334
|
+
const depositCall = this.contract.populate("deposit", [
|
|
335
|
+
uint256.bnToUint256(amountInfo.amount.toWei()),
|
|
336
|
+
receiver.address,
|
|
337
|
+
]);
|
|
338
|
+
return [approvalCall, depositCall];
|
|
339
|
+
}catch(err){
|
|
340
|
+
console.error("Error depositing:", err);
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async getVaultStatus(): Promise<YoloVaultStatus> {
|
|
346
|
+
const vaultStatus = await this.contract.call("get_vault_status", []);
|
|
347
|
+
return vaultStatus as YoloVaultStatus;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async matchInputAmounts(
|
|
351
|
+
amountInfo: DualActionAmount,
|
|
352
|
+
user: ContractAddr,
|
|
353
|
+
): Promise<DualActionAmount> {
|
|
354
|
+
let { primaryTokenBalance: redeemableBaseTokenAmount, claimableSecondaryTokens: redeemableSecondaryTokenAmount } =
|
|
355
|
+
await this.getNormalizedUserInfo(user);
|
|
356
|
+
redeemableBaseTokenAmount = new Web3Number(redeemableBaseTokenAmount.toWei().toString(), 0);
|
|
357
|
+
redeemableSecondaryTokenAmount = new Web3Number(redeemableSecondaryTokenAmount.toWei().toString(), 0);
|
|
358
|
+
const withdrawRequest = this.resolveWithdrawRequest(
|
|
359
|
+
amountInfo,
|
|
360
|
+
redeemableBaseTokenAmount,
|
|
361
|
+
redeemableSecondaryTokenAmount,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (!withdrawRequest) {
|
|
365
|
+
throw new Error("Invalid amount info");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const {
|
|
369
|
+
sharesUsedFactor,
|
|
370
|
+
baseTokenAmountToWithdraw,
|
|
371
|
+
secondaryTokenAmountToWithdraw,
|
|
372
|
+
} = withdrawRequest;
|
|
373
|
+
|
|
374
|
+
if (baseTokenAmountToWithdraw > 0) {
|
|
375
|
+
const secondaryTokenAmount = redeemableSecondaryTokenAmount.dividedBy(10 ** this.secondaryToken.decimals).multipliedBy(sharesUsedFactor);
|
|
376
|
+
return {
|
|
377
|
+
token0: {
|
|
378
|
+
tokenInfo: amountInfo.token0.tokenInfo,
|
|
379
|
+
amount: new Web3Number(baseTokenAmountToWithdraw.toString(), 0),
|
|
380
|
+
},
|
|
381
|
+
token1: {
|
|
382
|
+
tokenInfo: amountInfo.token1.tokenInfo,
|
|
383
|
+
amount: secondaryTokenAmount,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const baseTokenAmount = redeemableBaseTokenAmount.dividedBy(10 ** this.primaryToken.decimals).multipliedBy(sharesUsedFactor);
|
|
389
|
+
return {
|
|
390
|
+
token0: {
|
|
391
|
+
tokenInfo: amountInfo.token0.tokenInfo,
|
|
392
|
+
amount: baseTokenAmount,
|
|
393
|
+
},
|
|
394
|
+
token1: {
|
|
395
|
+
tokenInfo: amountInfo.token1.tokenInfo,
|
|
396
|
+
amount: new Web3Number(secondaryTokenAmountToWithdraw.toString(), 0),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async withdrawCall(
|
|
402
|
+
amountInfo: DualActionAmount,
|
|
403
|
+
receiver: ContractAddr,
|
|
404
|
+
owner: ContractAddr,
|
|
405
|
+
): Promise<Call[]> {
|
|
406
|
+
try{
|
|
407
|
+
let {
|
|
408
|
+
shares: userShares,
|
|
409
|
+
primaryTokenBalance: redeemableBaseTokenAmount,
|
|
410
|
+
claimableSecondaryTokens: redeemableSecondaryTokenAmount,
|
|
411
|
+
} = await this.getNormalizedUserInfo(receiver);
|
|
412
|
+
redeemableBaseTokenAmount = new Web3Number(redeemableBaseTokenAmount.toWei().toString(), 0);
|
|
413
|
+
redeemableSecondaryTokenAmount = new Web3Number(redeemableSecondaryTokenAmount.toWei().toString(), 0);
|
|
414
|
+
const withdrawRequest = this.resolveWithdrawRequest(
|
|
415
|
+
amountInfo,
|
|
416
|
+
redeemableBaseTokenAmount,
|
|
417
|
+
redeemableSecondaryTokenAmount,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (!withdrawRequest) {
|
|
421
|
+
throw new Error("Invalid amount info");
|
|
422
|
+
}
|
|
423
|
+
const requiredShares = userShares.multipliedBy(withdrawRequest.sharesUsedFactor).floor();
|
|
424
|
+
let withdrawCall = this.contract.populate("redeem", [
|
|
425
|
+
uint256.bnToUint256(requiredShares.toString()),
|
|
426
|
+
receiver.address,
|
|
427
|
+
]);
|
|
428
|
+
return [withdrawCall];
|
|
429
|
+
}catch(err){
|
|
430
|
+
console.error("Error withdrawing:", err);
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async netAPY(): Promise<number | string | NetAPYDetails> {
|
|
436
|
+
return "🤙YOLO"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async getSwapAmounts(spendUnits: Web3Number): Promise<{ grossSpend: Web3Number; netSpend: Web3Number; isReadyForNextSwap: boolean }> {
|
|
440
|
+
const swapAmounts: any = await this.contract.call("get_swap_amounts", [spendUnits.toUint256()]);
|
|
441
|
+
console.log("swapAmounts", swapAmounts);
|
|
442
|
+
return {
|
|
443
|
+
grossSpend: Web3Number.fromWei(swapAmounts[0].toString(), this.primaryToken.decimals),
|
|
444
|
+
netSpend: Web3Number.fromWei(swapAmounts[1].toString(), this.primaryToken.decimals),
|
|
445
|
+
isReadyForNextSwap: swapAmounts[2] as boolean,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
getSettings = async (): Promise<YoloSettings> => {
|
|
450
|
+
const settings = await this.contract.call("get_settings", []);
|
|
451
|
+
return settings as YoloSettings;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
async getUserPositionCards(input: UserPositionCardsInput): Promise<UserPositionCard[]> {
|
|
455
|
+
const userTVL = await this.getUserTVL(input.user);
|
|
456
|
+
const holdingsTitle = `${this.primaryToken.symbol} Left`;
|
|
457
|
+
const earningsTitle = `${this.secondaryToken.symbol} Accumulated`;
|
|
458
|
+
const cards: UserPositionCard[] = [
|
|
459
|
+
{
|
|
460
|
+
title: "Your Holdings",
|
|
461
|
+
tooltip: "Combined value of your remaining base token and accumulated secondary token",
|
|
462
|
+
value: this.formatUSDForCard(userTVL.usdValue),
|
|
463
|
+
subValue: `${holdingsTitle} + ${earningsTitle}`,
|
|
464
|
+
subValueColor: "positive",
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
title: holdingsTitle,
|
|
468
|
+
tooltip: `Amount of ${this.primaryToken.symbol} left in the vault, waiting to be swapped`,
|
|
469
|
+
value: this.formatTokenAmountForCard(userTVL.token0.amount, userTVL.token0.tokenInfo),
|
|
470
|
+
subValue: `≈ ${this.formatUSDForCard(userTVL.token0.usdValue)}`,
|
|
471
|
+
subValueColor: "positive",
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
title: earningsTitle,
|
|
475
|
+
tooltip: `Amount of ${this.secondaryToken.symbol} accumulated in the vault`,
|
|
476
|
+
value: this.formatTokenAmountForCard(userTVL.token1.amount, userTVL.token1.tokenInfo),
|
|
477
|
+
subValue: `≈ ${this.formatUSDForCard(userTVL.token1.usdValue)}`,
|
|
478
|
+
subValueColor: "default",
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
cards.push(await this.createApyCard(input));
|
|
482
|
+
return cards;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const formatPriceLabel = (price: number): string => {
|
|
487
|
+
return `${price.toLocaleString("en-US")}`;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const formatDurationSeconds = (seconds: number): string => {
|
|
491
|
+
return `${Math.floor(seconds / 3600).toLocaleString("en-US")} hours`;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const getLevelRangeLabel = (
|
|
495
|
+
level: YoloSpendingLevel,
|
|
496
|
+
secondaryTokenSymbol: string,
|
|
497
|
+
): string => {
|
|
498
|
+
if (level.minPrice !== undefined && level.maxPrice !== undefined) {
|
|
499
|
+
return `${formatPriceLabel(level.minPrice)}-${formatPriceLabel(level.maxPrice)} ${secondaryTokenSymbol}`;
|
|
500
|
+
}
|
|
501
|
+
if (level.minPrice !== undefined) {
|
|
502
|
+
return `>= ${formatPriceLabel(level.minPrice)} ${secondaryTokenSymbol}`;
|
|
503
|
+
}
|
|
504
|
+
return `< ${formatPriceLabel(level.maxPrice!)} ${secondaryTokenSymbol}`;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const getSpendingLevelRows = (
|
|
508
|
+
levels: YoloSpendingLevel[],
|
|
509
|
+
secondaryTokenSymbol: string,
|
|
510
|
+
): Array<{ range: string; multiplier: string }> => {
|
|
511
|
+
const positiveSpends = levels.map((l) => l.spendPercent).filter((v) => v > 0);
|
|
512
|
+
const minSpendPercent = positiveSpends.length > 0 ? Math.min(...positiveSpends) : 1;
|
|
513
|
+
|
|
514
|
+
return levels.map((level) => {
|
|
515
|
+
const multiplier = level.spendPercent > 0 ? level.spendPercent / minSpendPercent : 0;
|
|
516
|
+
return {
|
|
517
|
+
range: getLevelRangeLabel(level, secondaryTokenSymbol),
|
|
518
|
+
multiplier: `${multiplier.toFixed(2)}x`,
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const getYoloVaultCopy = (input: YoloVaultSettings) => {
|
|
524
|
+
const main = input.mainToken.symbol;
|
|
525
|
+
const secondary = input.secondaryToken.symbol;
|
|
526
|
+
const spendingRows = getSpendingLevelRows(input.spendingLevels, secondary);
|
|
527
|
+
const description = createElement(
|
|
528
|
+
"div",
|
|
529
|
+
{
|
|
530
|
+
style: {
|
|
531
|
+
display: "flex",
|
|
532
|
+
flexDirection: "column",
|
|
533
|
+
gap: "12px",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
createElement(
|
|
537
|
+
"p",
|
|
538
|
+
null,
|
|
539
|
+
`Troves Value Averaging (TVA) vault for ${main} -> ${secondary} accumulation.`
|
|
540
|
+
),
|
|
541
|
+
createElement(
|
|
542
|
+
"p",
|
|
543
|
+
null,
|
|
544
|
+
`We're all bullish on ${secondary}. Price only dips to hit new ATHs in the next cycle. This vault helps you prep - degen style. After all, YOLO.`
|
|
545
|
+
),
|
|
546
|
+
createElement("p", null, createElement("strong", null, "Start Date: "), input.startDate),
|
|
547
|
+
createElement("p", null, createElement("strong", null, "Expiry Date: "), input.expiryDate),
|
|
548
|
+
createElement(
|
|
549
|
+
"p",
|
|
550
|
+
null,
|
|
551
|
+
createElement("strong", null, "Execution Window: "),
|
|
552
|
+
`${input.totalEpochs} epochs, minimum ${formatDurationSeconds(input.minEpochDurationSeconds)} per epoch`
|
|
553
|
+
),
|
|
554
|
+
createElement(
|
|
555
|
+
"p",
|
|
556
|
+
null,
|
|
557
|
+
createElement("strong", null, "TVA Edge vs DCA: "),
|
|
558
|
+
"deploys more aggressively into dips and stays measured when prices are high."
|
|
559
|
+
),
|
|
560
|
+
createElement("div", null, createElement("strong", null, "Spend Levels: ")),
|
|
561
|
+
createElement(
|
|
562
|
+
"div",
|
|
563
|
+
{
|
|
564
|
+
style: {
|
|
565
|
+
display: "grid",
|
|
566
|
+
gridTemplateColumns: "2fr 1fr",
|
|
567
|
+
gap: "4px 12px",
|
|
568
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
569
|
+
fontSize: "12px",
|
|
570
|
+
lineHeight: "18px",
|
|
571
|
+
padding: "8px 10px",
|
|
572
|
+
borderRadius: "8px",
|
|
573
|
+
background: "rgba(255,255,255,0.04)",
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
createElement("strong", { key: "h-range" }, "Range"),
|
|
577
|
+
createElement("strong", { key: "h-mult" }, "Multiplier"),
|
|
578
|
+
...spendingRows.flatMap((row, idx) => [
|
|
579
|
+
createElement("span", { key: `r-${idx}` }, row.range),
|
|
580
|
+
createElement("span", { key: `m-${idx}` }, row.multiplier),
|
|
581
|
+
])
|
|
582
|
+
),
|
|
583
|
+
createElement(
|
|
584
|
+
"p",
|
|
585
|
+
null,
|
|
586
|
+
"Learn the core ",
|
|
587
|
+
highlightTextWithLinks("value averaging", [
|
|
588
|
+
{
|
|
589
|
+
highlight: "value averaging",
|
|
590
|
+
link: "https://www.investopedia.com/terms/v/value_averaging.asp",
|
|
591
|
+
},
|
|
592
|
+
]),
|
|
593
|
+
" concept."
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
const vaultTypeDescription =
|
|
597
|
+
`Troves Value Averaging (TVA) vault to accumulate ${secondary} from ${main} using level-based buying until ${input.expiryDate}.`;
|
|
598
|
+
const faqs = [
|
|
599
|
+
{
|
|
600
|
+
question: `What is this ${secondary} TVA vault?`,
|
|
601
|
+
answer:
|
|
602
|
+
`You deposit ${main}, and troves executes epoch swaps to accumulate ${secondary} based on configured spend levels until ${input.expiryDate}.`,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
question: `Why TVA instead of standard DCA?`,
|
|
606
|
+
answer:
|
|
607
|
+
"TVA increases buying intensity as price drops and can stay conservative at higher prices. This dynamic schedule can outperform fixed-size DCA in volatile markets.",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
question: `When does this vault expire (${input.expiryDate})?`,
|
|
611
|
+
answer:
|
|
612
|
+
`The active schedule starts on ${input.startDate}, runs for ${input.totalEpochs} epochs, and each epoch needs at least ${formatDurationSeconds(input.minEpochDurationSeconds)} before the next execution.`,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
question: "How are spend levels applied?",
|
|
616
|
+
answer:
|
|
617
|
+
`Each epoch uses a spend band from 0-100% of allowed units based on ${secondary}/${main} market conditions. Lower the price, higher the spend percentage.`,
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
question: "How do withdrawals work?",
|
|
621
|
+
answer:
|
|
622
|
+
`Redemption returns your pro-rata remaining ${main} and accumulated ${secondary} based on shares.`,
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
question: "Is this strategy audited and open source?",
|
|
626
|
+
answer:
|
|
627
|
+
"Not yet. The strategy is currently not audited and the code is closed source.",
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
question: "What are the fees in this vault?",
|
|
631
|
+
answer:
|
|
632
|
+
"This Vault has two fees: Swap fee (0.5%) (similar to management fee) and Performance fee (10%). Swap fee covers execution and routing costs each epoch. Performance fee applies only when exits are in profit. If TVL scales, fee reduction or removal is on the table.",
|
|
633
|
+
},
|
|
634
|
+
];
|
|
635
|
+
const investmentSteps = [
|
|
636
|
+
`Deposit ${main} after start (${input.startDate}) and before expiry (${input.expiryDate}).`,
|
|
637
|
+
`Each epoch (minimum ${formatDurationSeconds(input.minEpochDurationSeconds)}), troves swaps into ${secondary} based on the configured spending level.`,
|
|
638
|
+
`Lower prices can trigger higher spend percentages (TVA behavior).`,
|
|
639
|
+
`Redeem any time to receive your remaining ${main} plus accumulated ${secondary}.`,
|
|
640
|
+
`On expiry, entire ${main} token would have been swapped into ${secondary}, unless due to unfavourable market conditions.`
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
title: `${secondary} YOLO (${input.expiryDate})`,
|
|
645
|
+
description,
|
|
646
|
+
vaultTypeDescription,
|
|
647
|
+
faqs,
|
|
648
|
+
investmentSteps,
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const usdc = Global.getDefaultTokens().find((t) => t.symbol === "USDC")!;
|
|
653
|
+
const wbtc = Global.getDefaultTokens().find((t) => t.symbol === "WBTC")!;
|
|
654
|
+
|
|
655
|
+
const btcYoloConfig: YoloVaultStrategyConfig = {
|
|
656
|
+
id: `btc-yolo-31-dec-2026`,
|
|
657
|
+
address: ContractAddr.from("0x018ccdff25a642e211f86ace35ba282ebdf342330319ead98cae37258bc9cce1"),
|
|
658
|
+
startDate: "03-MAR-2026",
|
|
659
|
+
mainToken: usdc,
|
|
660
|
+
secondaryToken: wbtc,
|
|
661
|
+
expiryDate: "31-DEC-2026",
|
|
662
|
+
totalEpochs: 1206407,
|
|
663
|
+
minEpochDurationSeconds: 21600,
|
|
664
|
+
feeBps: 50,
|
|
665
|
+
spendingLevels: [
|
|
666
|
+
{ minPrice: 80000, maxPrice: 100000, spendPercent: 50 },
|
|
667
|
+
{ minPrice: 70000, maxPrice: 80000, spendPercent: 70 },
|
|
668
|
+
{ minPrice: 60000, maxPrice: 70000, spendPercent: 100 },
|
|
669
|
+
{ minPrice: 50000, maxPrice: 60000, spendPercent: 250 },
|
|
670
|
+
{ maxPrice: 50000, spendPercent: 500 },
|
|
671
|
+
],
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const yoloCopy = getYoloVaultCopy(btcYoloConfig);
|
|
675
|
+
|
|
676
|
+
// Risk factors intentionally left unrated for this strategy for now.
|
|
677
|
+
// const yoloRiskFactors: RiskFactor[] = [ ... ];
|
|
678
|
+
const yoloRiskFactors: RiskFactor[] = [];
|
|
679
|
+
|
|
680
|
+
export const YoloVaultStrategies: IStrategyMetadata<YoloVaultSettings>[] = [
|
|
681
|
+
{
|
|
682
|
+
id: btcYoloConfig.id,
|
|
683
|
+
name: yoloCopy.title,
|
|
684
|
+
description: yoloCopy.description,
|
|
685
|
+
address: btcYoloConfig.address,
|
|
686
|
+
vaultType: {
|
|
687
|
+
type: VaultType.TVA,
|
|
688
|
+
description: yoloCopy.vaultTypeDescription,
|
|
689
|
+
},
|
|
690
|
+
curator: UnwrapLabsCurator,
|
|
691
|
+
security: {
|
|
692
|
+
auditStatus: AuditStatus.NOT_AUDITED,
|
|
693
|
+
sourceCode: {
|
|
694
|
+
type: SourceCodeType.CLOSED_SOURCE,
|
|
695
|
+
contractLink: "",
|
|
696
|
+
},
|
|
697
|
+
accessControl: {
|
|
698
|
+
type: AccessControlType.ROLE_BASED_ACCESS,
|
|
699
|
+
addresses: [MY_ACCESS_CONTROL.address],
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
redemptionInfo: {
|
|
703
|
+
instantWithdrawalVault: InstantWithdrawalVault.YES,
|
|
704
|
+
redemptionsInfo: [],
|
|
705
|
+
alerts: [],
|
|
706
|
+
},
|
|
707
|
+
usualTimeToEarnings: null,
|
|
708
|
+
usualTimeToEarningsDescription: null,
|
|
709
|
+
launchBlock: 0,
|
|
710
|
+
type: "Other",
|
|
711
|
+
depositTokens: [
|
|
712
|
+
btcYoloConfig.mainToken,
|
|
713
|
+
btcYoloConfig.secondaryToken,
|
|
714
|
+
],
|
|
715
|
+
protocols: [],
|
|
716
|
+
risk: {
|
|
717
|
+
riskFactor: yoloRiskFactors,
|
|
718
|
+
netRisk: 0,
|
|
719
|
+
notARisks: getNoRiskTags(yoloRiskFactors),
|
|
720
|
+
},
|
|
721
|
+
additionalInfo: {
|
|
722
|
+
mainToken: btcYoloConfig.mainToken,
|
|
723
|
+
secondaryToken: btcYoloConfig.secondaryToken,
|
|
724
|
+
startDate: btcYoloConfig.startDate,
|
|
725
|
+
expiryDate: btcYoloConfig.expiryDate,
|
|
726
|
+
totalEpochs: btcYoloConfig.totalEpochs,
|
|
727
|
+
minEpochDurationSeconds: btcYoloConfig.minEpochDurationSeconds,
|
|
728
|
+
spendingLevels: btcYoloConfig.spendingLevels,
|
|
729
|
+
feeBps: btcYoloConfig.feeBps, // swap fee bps
|
|
730
|
+
},
|
|
731
|
+
faqs: yoloCopy.faqs,
|
|
732
|
+
contractDetails: [],
|
|
733
|
+
investmentSteps: yoloCopy.investmentSteps,
|
|
734
|
+
settings: {
|
|
735
|
+
liveStatus: StrategyLiveStatus.HOT,
|
|
736
|
+
isAudited: false,
|
|
737
|
+
quoteToken: btcYoloConfig.mainToken,
|
|
738
|
+
isTransactionHistDisabled: true,
|
|
739
|
+
maxTVL: new Web3Number("200000", 6),
|
|
740
|
+
},
|
|
741
|
+
apyHistoryUIConfig: {
|
|
742
|
+
showApyHistory: false,
|
|
743
|
+
noApyHistoryMessage:
|
|
744
|
+
"APY history is hidden because this is a TVA accumulation vault, not a yield-bearing APY strategy.",
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
];
|