@strkfarm/sdk 2.0.0-staging.7 → 2.0.0-staging.71
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/index.browser.global.js +3150 -1041
- package/dist/index.browser.mjs +2839 -722
- package/dist/index.d.ts +351 -50
- package/dist/index.js +2934 -805
- package/dist/index.mjs +2842 -722
- package/package.json +4 -4
- package/src/data/universal-vault.abi.json +143 -27
- 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 +61 -8
- package/src/interfaces/common.tsx +85 -27
- package/src/modules/avnu.ts +1 -1
- package/src/modules/erc20.ts +18 -2
- package/src/modules/index.ts +3 -1
- package/src/modules/pricer-avnu-api.ts +114 -0
- package/src/modules/pricer.ts +76 -46
- package/src/node/pricer-redis.ts +1 -0
- package/src/strategies/base-strategy.ts +153 -8
- package/src/strategies/constants.ts +2 -2
- package/src/strategies/ekubo-cl-vault.tsx +257 -91
- 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 +52 -13
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +46 -25
- package/src/strategies/universal-lst-muliplier-strategy.tsx +1464 -584
- package/src/strategies/universal-strategy.tsx +160 -81
- package/src/strategies/vesu-rebalance.tsx +22 -12
- package/src/strategies/yoloVault.ts +1084 -0
- package/src/utils/strategy-utils.ts +6 -2
|
@@ -0,0 +1,1084 @@
|
|
|
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 { BlockIdentifier, uint256, 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 ERC4626Abi from "@/data/erc4626.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
|
+
import { Token } from "graphql";
|
|
40
|
+
|
|
41
|
+
export interface YoloVaultSettings {
|
|
42
|
+
startDate: string;
|
|
43
|
+
expiryDate: string;
|
|
44
|
+
mainToken: TokenInfo;
|
|
45
|
+
secondaryToken: TokenInfo;
|
|
46
|
+
totalEpochs: number;
|
|
47
|
+
minEpochDurationSeconds: number;
|
|
48
|
+
spendingLevels: YoloSpendingLevel[];
|
|
49
|
+
feeBps: number; // in bps
|
|
50
|
+
/** When true, base token is ERC-4626 (e.g. vUSDC); amounts for TVL / user info use `convert_to_assets` into `baseUnderlying`. */
|
|
51
|
+
isBaseERC4626?: boolean;
|
|
52
|
+
/** When true, second token is ERC-4626 (e.g. xSTRK); amounts use `convert_to_assets` into `secondUnderlying`. */
|
|
53
|
+
isSecondERC4626?: boolean;
|
|
54
|
+
/** Required when `isBaseERC4626` is true (e.g. USDC). */
|
|
55
|
+
baseUnderlying?: TokenInfo;
|
|
56
|
+
/** Required when `isSecondERC4626` is true (e.g. STRK / WBTC for xSTRK / xWBTC). */
|
|
57
|
+
secondUnderlying?: TokenInfo;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface YoloSpendingLevel {
|
|
61
|
+
minPrice?: number;
|
|
62
|
+
maxPrice?: number;
|
|
63
|
+
spendPercent: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface YoloVaultStrategyConfig extends YoloVaultSettings {
|
|
67
|
+
id: string;
|
|
68
|
+
address: ContractAddr;
|
|
69
|
+
parent_id: string;
|
|
70
|
+
/** Higher shows earlier in UI. */
|
|
71
|
+
priority?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UserYoloInfo {
|
|
75
|
+
shares: bigint;
|
|
76
|
+
claimable_second_tokens: bigint;
|
|
77
|
+
base_token_balance: bigint;
|
|
78
|
+
base_token_consumed: bigint;
|
|
79
|
+
base_consumed_last_index: bigint;
|
|
80
|
+
second_token_last_index: bigint;
|
|
81
|
+
second_token_balance: bigint;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface YoloVaultStatus {
|
|
85
|
+
current_epoch: bigint;
|
|
86
|
+
total_epochs: bigint;
|
|
87
|
+
remaining_base: bigint;
|
|
88
|
+
total_second_tokens: bigint;
|
|
89
|
+
global_second_token_index: bigint;
|
|
90
|
+
cumulative_spend_index: bigint;
|
|
91
|
+
total_shares: bigint;
|
|
92
|
+
base_token_assets_per_share: bigint;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface YoloSettings {
|
|
96
|
+
base_token: bigint;
|
|
97
|
+
second_token: bigint;
|
|
98
|
+
total_epochs: bigint;
|
|
99
|
+
min_time_per_epoch: bigint;
|
|
100
|
+
max_spend_units_per_epoch: bigint;
|
|
101
|
+
base_token_assets_per_share: bigint;
|
|
102
|
+
oracle: bigint;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type YoloErc4626RuntimeConfig = {
|
|
106
|
+
isBaseERC4626: boolean;
|
|
107
|
+
isSecondERC4626: boolean;
|
|
108
|
+
baseUnderlying?: TokenInfo;
|
|
109
|
+
secondUnderlying?: TokenInfo;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export class YoLoVault extends BaseStrategy<DualTokenInfo, SingleActionAmount, DualActionAmount> {
|
|
113
|
+
readonly address: ContractAddr;
|
|
114
|
+
readonly metadata: IStrategyMetadata<YoloVaultSettings>;
|
|
115
|
+
readonly pricer: PricerBase;
|
|
116
|
+
/** Resolves to a `Contract` built from `provider.getClassAt` at the vault address (no checked-in vault ABI). */
|
|
117
|
+
readonly contract: Promise<Contract>;
|
|
118
|
+
readonly primaryToken : TokenInfo;
|
|
119
|
+
readonly secondaryToken : TokenInfo;
|
|
120
|
+
readonly erc4626: YoloErc4626RuntimeConfig;
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
config: IConfig,
|
|
124
|
+
pricer: PricerBase,
|
|
125
|
+
metadata: IStrategyMetadata<YoloVaultSettings>,
|
|
126
|
+
) {
|
|
127
|
+
super(config, {
|
|
128
|
+
depositInputMode: "single",
|
|
129
|
+
withdrawInputMode: "dual",
|
|
130
|
+
});
|
|
131
|
+
this.address = metadata.address;
|
|
132
|
+
this.pricer = pricer;
|
|
133
|
+
this.metadata = metadata;
|
|
134
|
+
this.contract = this.config.provider
|
|
135
|
+
.getClassAt(this.address.address)
|
|
136
|
+
.then(
|
|
137
|
+
(cls) =>
|
|
138
|
+
new Contract({
|
|
139
|
+
abi: cls.abi,
|
|
140
|
+
address: this.address.address,
|
|
141
|
+
providerOrAccount: this.config.provider,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (metadata.depositTokens.length < 1) {
|
|
146
|
+
throw new Error("Deposit tokens are not fully defined in metadata");
|
|
147
|
+
}
|
|
148
|
+
this.primaryToken = metadata.additionalInfo.mainToken;
|
|
149
|
+
this.secondaryToken = metadata.additionalInfo.secondaryToken;
|
|
150
|
+
const ai = metadata.additionalInfo;
|
|
151
|
+
this.erc4626 = {
|
|
152
|
+
isBaseERC4626: ai.isBaseERC4626 ?? false,
|
|
153
|
+
isSecondERC4626: ai.isSecondERC4626 ?? false,
|
|
154
|
+
baseUnderlying: ai.baseUnderlying,
|
|
155
|
+
secondUnderlying: ai.secondUnderlying,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Underlying (or base token) used for pricing / swap sell leg when base is ERC-4626. */
|
|
160
|
+
tokenForPrimaryPricing(): TokenInfo {
|
|
161
|
+
return this.erc4626.isBaseERC4626 && this.erc4626.baseUnderlying
|
|
162
|
+
? this.erc4626.baseUnderlying
|
|
163
|
+
: this.primaryToken;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Underlying (or second token) for price ratios when second leg is ERC-4626 (e.g. STRK for xSTRK). */
|
|
167
|
+
tokenForSecondaryPricing(): TokenInfo {
|
|
168
|
+
return this.erc4626.isSecondERC4626 && this.erc4626.secondUnderlying
|
|
169
|
+
? this.erc4626.secondUnderlying
|
|
170
|
+
: this.secondaryToken;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private primaryAmountDecimals(): number {
|
|
174
|
+
return this.erc4626.isBaseERC4626 && this.erc4626.baseUnderlying
|
|
175
|
+
? this.erc4626.baseUnderlying.decimals
|
|
176
|
+
: this.primaryToken.decimals;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private secondaryAmountDecimals(): number {
|
|
180
|
+
return this.erc4626.isSecondERC4626 && this.erc4626.secondUnderlying
|
|
181
|
+
? this.erc4626.secondUnderlying.decimals
|
|
182
|
+
: this.secondaryToken.decimals;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async convertWrapperSharesToUnderlying(
|
|
186
|
+
wrapper: TokenInfo,
|
|
187
|
+
sharesRaw: bigint,
|
|
188
|
+
assetDecimals: number,
|
|
189
|
+
blockIdentifier: BlockIdentifier,
|
|
190
|
+
): Promise<Web3Number> {
|
|
191
|
+
const wrapperContract = new Contract({
|
|
192
|
+
abi: ERC4626Abi,
|
|
193
|
+
address: wrapper.address.address,
|
|
194
|
+
providerOrAccount: this.config.provider,
|
|
195
|
+
});
|
|
196
|
+
const assets = await wrapperContract.call(
|
|
197
|
+
"convert_to_assets",
|
|
198
|
+
[uint256.bnToUint256(sharesRaw.toString())],
|
|
199
|
+
{ blockIdentifier },
|
|
200
|
+
);
|
|
201
|
+
return Web3Number.fromWei(assets.toString(), assetDecimals);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// formatTokenAmount(amount: Web3Number, decimals: number): Web3Number {
|
|
205
|
+
// const formattedAmount = amount.dividedBy(10 ** decimals);
|
|
206
|
+
// return formattedAmount;
|
|
207
|
+
// }
|
|
208
|
+
|
|
209
|
+
private async getNormalizedUserInfo(user: ContractAddr, blockIdentifier: BlockIdentifier = "latest"): Promise<{
|
|
210
|
+
shares: Web3Number;
|
|
211
|
+
primaryTokenBalance: Web3Number;
|
|
212
|
+
secondaryTokenBalance: Web3Number;
|
|
213
|
+
claimableSecondaryTokens: Web3Number;
|
|
214
|
+
}> {
|
|
215
|
+
const userInfo = await (await this.contract).call("get_user_info", [user.address], {
|
|
216
|
+
blockIdentifier,
|
|
217
|
+
});
|
|
218
|
+
const {
|
|
219
|
+
shares,
|
|
220
|
+
base_token_balance,
|
|
221
|
+
second_token_balance,
|
|
222
|
+
claimable_second_tokens,
|
|
223
|
+
} = userInfo as UserYoloInfo;
|
|
224
|
+
const userShares = new Web3Number(shares.toString(), 0);
|
|
225
|
+
const baseShares = base_token_balance;
|
|
226
|
+
const secondShares = second_token_balance;
|
|
227
|
+
const claimSecondShares = claimable_second_tokens;
|
|
228
|
+
|
|
229
|
+
let baseTokenBalance: Web3Number;
|
|
230
|
+
if (this.erc4626.isBaseERC4626 && this.erc4626.baseUnderlying) {
|
|
231
|
+
baseTokenBalance = await this.convertWrapperSharesToUnderlying(
|
|
232
|
+
this.primaryToken,
|
|
233
|
+
baseShares,
|
|
234
|
+
this.erc4626.baseUnderlying.decimals,
|
|
235
|
+
blockIdentifier,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
baseTokenBalance = Web3Number.fromWei(baseShares.toString(), this.primaryToken.decimals);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let secondTokenBalance: Web3Number;
|
|
242
|
+
let claimableSecondTokens: Web3Number;
|
|
243
|
+
if (this.erc4626.isSecondERC4626 && this.erc4626.secondUnderlying) {
|
|
244
|
+
const assetDec = this.erc4626.secondUnderlying.decimals;
|
|
245
|
+
secondTokenBalance = await this.convertWrapperSharesToUnderlying(
|
|
246
|
+
this.secondaryToken,
|
|
247
|
+
secondShares,
|
|
248
|
+
assetDec,
|
|
249
|
+
blockIdentifier,
|
|
250
|
+
);
|
|
251
|
+
claimableSecondTokens = await this.convertWrapperSharesToUnderlying(
|
|
252
|
+
this.secondaryToken,
|
|
253
|
+
claimSecondShares,
|
|
254
|
+
assetDec,
|
|
255
|
+
blockIdentifier,
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
secondTokenBalance = Web3Number.fromWei(secondShares.toString(), this.secondaryToken.decimals);
|
|
259
|
+
claimableSecondTokens = Web3Number.fromWei(claimSecondShares.toString(), this.secondaryToken.decimals);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
shares: userShares,
|
|
264
|
+
primaryTokenBalance: baseTokenBalance,
|
|
265
|
+
secondaryTokenBalance: secondTokenBalance,
|
|
266
|
+
claimableSecondaryTokens: claimableSecondTokens,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private resolveWithdrawRequest(
|
|
271
|
+
amountInfo: DualActionAmount,
|
|
272
|
+
redeemableBaseTokenAmount: Web3Number,
|
|
273
|
+
redeemableSecondaryTokenAmount: Web3Number,
|
|
274
|
+
):
|
|
275
|
+
| {
|
|
276
|
+
sharesUsedFactor: number;
|
|
277
|
+
baseTokenAmountToWithdraw: number;
|
|
278
|
+
secondaryTokenAmountToWithdraw: number;
|
|
279
|
+
}
|
|
280
|
+
| null {
|
|
281
|
+
const baseTokenAmountToWithdraw = Number(amountInfo.token0.amount.toWei());
|
|
282
|
+
const secondaryTokenAmountToWithdraw = Number(amountInfo.token1.amount.toWei());
|
|
283
|
+
// if (baseTokenAmountToWithdraw > 0 && secondaryTokenAmountToWithdraw > 0) {
|
|
284
|
+
// throw new Error("Cannot pass amounts for both base and secondary tokens at once");
|
|
285
|
+
// }
|
|
286
|
+
if (
|
|
287
|
+
baseTokenAmountToWithdraw > 0 &&
|
|
288
|
+
redeemableBaseTokenAmount.greaterThanOrEqualTo(baseTokenAmountToWithdraw)
|
|
289
|
+
) {
|
|
290
|
+
const sharesUsedFactor = new Web3Number(baseTokenAmountToWithdraw.toString(), 0).dividedBy(redeemableBaseTokenAmount);
|
|
291
|
+
return {
|
|
292
|
+
sharesUsedFactor: sharesUsedFactor.toNumber(),
|
|
293
|
+
baseTokenAmountToWithdraw,
|
|
294
|
+
secondaryTokenAmountToWithdraw,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
secondaryTokenAmountToWithdraw > 0 &&
|
|
300
|
+
redeemableSecondaryTokenAmount.greaterThanOrEqualTo(
|
|
301
|
+
secondaryTokenAmountToWithdraw,
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
304
|
+
const sharesUsedFactor = new Web3Number(secondaryTokenAmountToWithdraw.toString(), 0).dividedBy(redeemableSecondaryTokenAmount);
|
|
305
|
+
return {
|
|
306
|
+
sharesUsedFactor: sharesUsedFactor.toNumber(),
|
|
307
|
+
baseTokenAmountToWithdraw,
|
|
308
|
+
secondaryTokenAmountToWithdraw,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getUserTVL(user: ContractAddr, blockIdentifier: BlockIdentifier = "latest"): Promise<DualTokenInfo> {
|
|
316
|
+
try {
|
|
317
|
+
const price0 = this.tokenForPrimaryPricing();
|
|
318
|
+
const price1 = this.tokenForSecondaryPricing();
|
|
319
|
+
const [{ primaryTokenBalance, claimableSecondaryTokens }, primaryTokenPrice, secondaryTokenPrice] = await Promise.all([
|
|
320
|
+
this.getNormalizedUserInfo(user, blockIdentifier),
|
|
321
|
+
this.pricer.getPrice(price0.symbol, blockIdentifier),
|
|
322
|
+
this.pricer.getPrice(price1.symbol, blockIdentifier),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
// ! todo u can simply do primaryTokenAmount.multipliedBy(primaryTokenPrice.price).toNumber()
|
|
326
|
+
// leaving the other unchanged to help u see the diff
|
|
327
|
+
const primaryTokenUsd = primaryTokenBalance.multipliedBy(primaryTokenPrice.price).toNumber();
|
|
328
|
+
const secondaryTokenUsd =claimableSecondaryTokens.multipliedBy(secondaryTokenPrice.price);
|
|
329
|
+
return {
|
|
330
|
+
usdValue: primaryTokenUsd + secondaryTokenUsd.toNumber(),
|
|
331
|
+
token0: {
|
|
332
|
+
tokenInfo: price0,
|
|
333
|
+
amount: primaryTokenBalance,
|
|
334
|
+
usdValue: primaryTokenUsd,
|
|
335
|
+
},
|
|
336
|
+
token1: {
|
|
337
|
+
tokenInfo: price1,
|
|
338
|
+
amount: claimableSecondaryTokens,
|
|
339
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("Error fetching user TVL:", error);
|
|
344
|
+
return {
|
|
345
|
+
usdValue: 0,
|
|
346
|
+
token0: {
|
|
347
|
+
tokenInfo: this.tokenForPrimaryPricing(),
|
|
348
|
+
amount: new Web3Number("0", this.primaryAmountDecimals()),
|
|
349
|
+
usdValue: 0,
|
|
350
|
+
},
|
|
351
|
+
token1: {
|
|
352
|
+
tokenInfo: this.tokenForSecondaryPricing(),
|
|
353
|
+
amount: new Web3Number("0", this.secondaryAmountDecimals()),
|
|
354
|
+
usdValue: 0,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getVaultPositions(): Promise<VaultPosition[]> {
|
|
361
|
+
const vaultStatus = await this.getVaultStatus();
|
|
362
|
+
const {
|
|
363
|
+
remaining_base: remainingBase,
|
|
364
|
+
total_second_tokens: totalSecondTokens,
|
|
365
|
+
} = vaultStatus as YoloVaultStatus;
|
|
366
|
+
const baseShares = BigInt(remainingBase.toString());
|
|
367
|
+
const secondShares = BigInt(totalSecondTokens.toString());
|
|
368
|
+
|
|
369
|
+
let primaryTokenAmount: Web3Number;
|
|
370
|
+
if (this.erc4626.isBaseERC4626 && this.erc4626.baseUnderlying) {
|
|
371
|
+
primaryTokenAmount = await this.convertWrapperSharesToUnderlying(
|
|
372
|
+
this.primaryToken,
|
|
373
|
+
baseShares,
|
|
374
|
+
this.erc4626.baseUnderlying.decimals,
|
|
375
|
+
"latest",
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
primaryTokenAmount = Web3Number.fromWei(remainingBase.toString(), this.primaryToken.decimals);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let secondaryTokenAmount: Web3Number;
|
|
382
|
+
if (this.erc4626.isSecondERC4626 && this.erc4626.secondUnderlying) {
|
|
383
|
+
secondaryTokenAmount = await this.convertWrapperSharesToUnderlying(
|
|
384
|
+
this.secondaryToken,
|
|
385
|
+
secondShares,
|
|
386
|
+
this.erc4626.secondUnderlying.decimals,
|
|
387
|
+
"latest",
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
secondaryTokenAmount = Web3Number.fromWei(totalSecondTokens.toString(), this.secondaryToken.decimals);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const displayPrimary = this.tokenForPrimaryPricing();
|
|
394
|
+
const displaySecondary = this.tokenForSecondaryPricing();
|
|
395
|
+
const [primaryTokenPrice, secondaryTokenPrice] = await Promise.all([
|
|
396
|
+
this.pricer.getPrice(displayPrimary.symbol),
|
|
397
|
+
this.pricer.getPrice(displaySecondary.symbol),
|
|
398
|
+
]);
|
|
399
|
+
const primaryTokenUsd = primaryTokenAmount.multipliedBy(primaryTokenPrice.price);
|
|
400
|
+
const secondaryTokenUsd = secondaryTokenAmount.multipliedBy(secondaryTokenPrice.price);
|
|
401
|
+
return [{
|
|
402
|
+
amount: primaryTokenAmount,
|
|
403
|
+
usdValue: primaryTokenUsd.toNumber(),
|
|
404
|
+
token: displayPrimary,
|
|
405
|
+
remarks: "Remaining deposit tokens in the Vault",
|
|
406
|
+
}, {
|
|
407
|
+
amount: secondaryTokenAmount,
|
|
408
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
409
|
+
token: displaySecondary,
|
|
410
|
+
remarks: "Total swapped tokens in the Vault",
|
|
411
|
+
}]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async getTVL(): Promise<DualTokenInfo> {
|
|
415
|
+
try {
|
|
416
|
+
const displayPrimary = this.tokenForPrimaryPricing();
|
|
417
|
+
const displaySecondary = this.tokenForSecondaryPricing();
|
|
418
|
+
const [vaultStatus, primaryTokenPrice, secondaryTokenPrice] =
|
|
419
|
+
await Promise.all([
|
|
420
|
+
this.getVaultStatus(),
|
|
421
|
+
this.pricer.getPrice(displayPrimary.symbol),
|
|
422
|
+
this.pricer.getPrice(displaySecondary.symbol),
|
|
423
|
+
]);
|
|
424
|
+
const {
|
|
425
|
+
remaining_base: remainingBase,
|
|
426
|
+
total_second_tokens: totalSecondTokens,
|
|
427
|
+
} = vaultStatus as YoloVaultStatus;
|
|
428
|
+
|
|
429
|
+
let primaryTokenAmount: Web3Number;
|
|
430
|
+
if (this.erc4626.isBaseERC4626 && this.erc4626.baseUnderlying) {
|
|
431
|
+
primaryTokenAmount = await this.convertWrapperSharesToUnderlying(
|
|
432
|
+
this.primaryToken,
|
|
433
|
+
BigInt(remainingBase.toString()),
|
|
434
|
+
this.erc4626.baseUnderlying.decimals,
|
|
435
|
+
"latest",
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
primaryTokenAmount = Web3Number.fromWei(remainingBase.toString(), this.primaryToken.decimals);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let secondaryTokenAmount: Web3Number;
|
|
442
|
+
if (this.erc4626.isSecondERC4626 && this.erc4626.secondUnderlying) {
|
|
443
|
+
secondaryTokenAmount = await this.convertWrapperSharesToUnderlying(
|
|
444
|
+
this.secondaryToken,
|
|
445
|
+
BigInt(totalSecondTokens.toString()),
|
|
446
|
+
this.erc4626.secondUnderlying.decimals,
|
|
447
|
+
"latest",
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
secondaryTokenAmount = Web3Number.fromWei(totalSecondTokens.toString(), this.secondaryToken.decimals);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const pDec = displayPrimary.decimals;
|
|
454
|
+
const sDec = displaySecondary.decimals;
|
|
455
|
+
const primaryTokenUsd = new Web3Number(
|
|
456
|
+
primaryTokenAmount.toFixed(pDec),
|
|
457
|
+
pDec,
|
|
458
|
+
).multipliedBy(primaryTokenPrice.price);
|
|
459
|
+
const secondaryTokenUsd = new Web3Number(
|
|
460
|
+
secondaryTokenAmount.toFixed(sDec),
|
|
461
|
+
sDec,
|
|
462
|
+
).multipliedBy(secondaryTokenPrice.price);
|
|
463
|
+
|
|
464
|
+
const totalUsdValue = primaryTokenUsd.plus(secondaryTokenUsd).toNumber();
|
|
465
|
+
|
|
466
|
+
if (
|
|
467
|
+
(totalUsdValue === 0 || primaryTokenAmount.eq(0) || secondaryTokenAmount.eq(0)) &&
|
|
468
|
+
this.metadata.settings?.liveStatus === StrategyLiveStatus.ACTIVE
|
|
469
|
+
) {
|
|
470
|
+
logger.warn(
|
|
471
|
+
`${this.metadata.name}:getTVL - Zero value detected: ` +
|
|
472
|
+
`usdValue=${totalUsdValue}, ` +
|
|
473
|
+
`primaryTokenAmount=${primaryTokenAmount.toString()}, ` +
|
|
474
|
+
`secondaryTokenAmount=${secondaryTokenAmount.toString()}, ` +
|
|
475
|
+
`primaryTokenPrice=${primaryTokenPrice.price}, ` +
|
|
476
|
+
`secondaryTokenPrice=${secondaryTokenPrice.price}, ` +
|
|
477
|
+
`primaryTokenUsd=${primaryTokenUsd.toNumber()}, ` +
|
|
478
|
+
`secondaryTokenUsd=${secondaryTokenUsd.toNumber()}`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
usdValue: totalUsdValue,
|
|
484
|
+
token0: {
|
|
485
|
+
tokenInfo: displayPrimary,
|
|
486
|
+
amount: primaryTokenAmount,
|
|
487
|
+
usdValue: primaryTokenUsd.toNumber(),
|
|
488
|
+
},
|
|
489
|
+
token1: {
|
|
490
|
+
tokenInfo: displaySecondary,
|
|
491
|
+
amount: secondaryTokenAmount,
|
|
492
|
+
usdValue: secondaryTokenUsd.toNumber(),
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error("Error fetching vault TVL:", error);
|
|
497
|
+
return {
|
|
498
|
+
usdValue: 0,
|
|
499
|
+
token0: {
|
|
500
|
+
tokenInfo: this.tokenForPrimaryPricing(),
|
|
501
|
+
amount: new Web3Number("0", this.primaryAmountDecimals()),
|
|
502
|
+
usdValue: 0,
|
|
503
|
+
},
|
|
504
|
+
token1: {
|
|
505
|
+
tokenInfo: this.tokenForSecondaryPricing(),
|
|
506
|
+
amount: new Web3Number("0", this.secondaryAmountDecimals()),
|
|
507
|
+
usdValue: 0,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async depositCall(
|
|
514
|
+
amountInfo: SingleActionAmount,
|
|
515
|
+
receiver: ContractAddr,
|
|
516
|
+
): Promise<Call[]> {
|
|
517
|
+
try{
|
|
518
|
+
const vault = await this.contract;
|
|
519
|
+
if (this.erc4626.isBaseERC4626) {
|
|
520
|
+
if (!this.erc4626.baseUnderlying) {
|
|
521
|
+
throw new Error("baseUnderlying missing for ERC-4626 base YOLO vault");
|
|
522
|
+
}
|
|
523
|
+
const approvalCall = new ERC20(this.config).approve(
|
|
524
|
+
this.erc4626.baseUnderlying.address.address,
|
|
525
|
+
this.address.address,
|
|
526
|
+
amountInfo.amount,
|
|
527
|
+
);
|
|
528
|
+
const depositCall = vault.populate("deposit_combined", [
|
|
529
|
+
uint256.bnToUint256(amountInfo.amount.toWei()),
|
|
530
|
+
receiver.address,
|
|
531
|
+
]);
|
|
532
|
+
return [approvalCall, depositCall];
|
|
533
|
+
}
|
|
534
|
+
const primaryToken = amountInfo.tokenInfo;
|
|
535
|
+
const approvalCall = new ERC20(this.config).approve(primaryToken.address.address, this.address.address, amountInfo.amount);
|
|
536
|
+
const depositCall = vault.populate("deposit", [
|
|
537
|
+
uint256.bnToUint256(amountInfo.amount.toWei()),
|
|
538
|
+
receiver.address,
|
|
539
|
+
]);
|
|
540
|
+
return [approvalCall, depositCall];
|
|
541
|
+
}catch(err){
|
|
542
|
+
console.error("Error depositing:", err);
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async getVaultStatus(): Promise<YoloVaultStatus> {
|
|
548
|
+
const vaultStatus = await (await this.contract).call("get_vault_status", []);
|
|
549
|
+
return vaultStatus as YoloVaultStatus;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async matchInputAmounts(
|
|
553
|
+
amountInfo: DualActionAmount,
|
|
554
|
+
user: ContractAddr,
|
|
555
|
+
): Promise<DualActionAmount> {
|
|
556
|
+
let { primaryTokenBalance: redeemableBaseTokenAmount, claimableSecondaryTokens: redeemableSecondaryTokenAmount } =
|
|
557
|
+
await this.getNormalizedUserInfo(user);
|
|
558
|
+
redeemableBaseTokenAmount = new Web3Number(redeemableBaseTokenAmount.toWei().toString(), 0);
|
|
559
|
+
redeemableSecondaryTokenAmount = new Web3Number(redeemableSecondaryTokenAmount.toWei().toString(), 0);
|
|
560
|
+
const withdrawRequest = this.resolveWithdrawRequest(
|
|
561
|
+
amountInfo,
|
|
562
|
+
redeemableBaseTokenAmount,
|
|
563
|
+
redeemableSecondaryTokenAmount,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
if (!withdrawRequest) {
|
|
567
|
+
throw new Error("Invalid amount info");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const {
|
|
571
|
+
sharesUsedFactor,
|
|
572
|
+
baseTokenAmountToWithdraw,
|
|
573
|
+
secondaryTokenAmountToWithdraw,
|
|
574
|
+
} = withdrawRequest;
|
|
575
|
+
|
|
576
|
+
if (baseTokenAmountToWithdraw > 0) {
|
|
577
|
+
const secondaryTokenAmount = redeemableSecondaryTokenAmount.dividedBy(10 ** this.secondaryAmountDecimals()).multipliedBy(sharesUsedFactor);
|
|
578
|
+
return {
|
|
579
|
+
token0: {
|
|
580
|
+
tokenInfo: amountInfo.token0.tokenInfo,
|
|
581
|
+
amount: new Web3Number(baseTokenAmountToWithdraw.toString(), 0),
|
|
582
|
+
},
|
|
583
|
+
token1: {
|
|
584
|
+
tokenInfo: amountInfo.token1.tokenInfo,
|
|
585
|
+
amount: secondaryTokenAmount,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const baseTokenAmount = redeemableBaseTokenAmount.dividedBy(10 ** this.primaryAmountDecimals()).multipliedBy(sharesUsedFactor);
|
|
591
|
+
return {
|
|
592
|
+
token0: {
|
|
593
|
+
tokenInfo: amountInfo.token0.tokenInfo,
|
|
594
|
+
amount: baseTokenAmount,
|
|
595
|
+
},
|
|
596
|
+
token1: {
|
|
597
|
+
tokenInfo: amountInfo.token1.tokenInfo,
|
|
598
|
+
amount: new Web3Number(secondaryTokenAmountToWithdraw.toString(), 0),
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async withdrawCall(
|
|
604
|
+
amountInfo: DualActionAmount,
|
|
605
|
+
receiver: ContractAddr,
|
|
606
|
+
owner: ContractAddr,
|
|
607
|
+
): Promise<Call[]> {
|
|
608
|
+
try{
|
|
609
|
+
let {
|
|
610
|
+
shares: userShares,
|
|
611
|
+
primaryTokenBalance: redeemableBaseTokenAmount,
|
|
612
|
+
claimableSecondaryTokens: redeemableSecondaryTokenAmount,
|
|
613
|
+
} = await this.getNormalizedUserInfo(receiver);
|
|
614
|
+
redeemableBaseTokenAmount = new Web3Number(redeemableBaseTokenAmount.toWei().toString(), 0);
|
|
615
|
+
redeemableSecondaryTokenAmount = new Web3Number(redeemableSecondaryTokenAmount.toWei().toString(), 0);
|
|
616
|
+
const withdrawRequest = this.resolveWithdrawRequest(
|
|
617
|
+
amountInfo,
|
|
618
|
+
redeemableBaseTokenAmount,
|
|
619
|
+
redeemableSecondaryTokenAmount,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (!withdrawRequest) {
|
|
623
|
+
throw new Error("Invalid amount info");
|
|
624
|
+
}
|
|
625
|
+
const requiredShares = userShares.multipliedBy(withdrawRequest.sharesUsedFactor).floor();
|
|
626
|
+
const redeemFn = this.erc4626.isBaseERC4626 ? "redeem_combined" : "redeem";
|
|
627
|
+
const vault = await this.contract;
|
|
628
|
+
let withdrawCall = vault.populate(redeemFn, [
|
|
629
|
+
uint256.bnToUint256(requiredShares.toString()),
|
|
630
|
+
receiver.address,
|
|
631
|
+
]);
|
|
632
|
+
return [withdrawCall];
|
|
633
|
+
}catch(err){
|
|
634
|
+
console.error("Error withdrawing:", err);
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async netAPY(): Promise<number | string | NetAPYDetails> {
|
|
640
|
+
return "🤙YOLO"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async getSwapAmounts(spendUnits: Web3Number): Promise<{ grossSpend: Web3Number; netSpend: Web3Number; isReadyForNextSwap: boolean }> {
|
|
644
|
+
const swapAmounts: any = await (await this.contract).call("get_swap_amounts", [spendUnits.toUint256()]);
|
|
645
|
+
console.log("swapAmounts", swapAmounts);
|
|
646
|
+
return {
|
|
647
|
+
grossSpend: Web3Number.fromWei(swapAmounts[0].toString(), this.primaryToken.decimals),
|
|
648
|
+
netSpend: Web3Number.fromWei(swapAmounts[1].toString(), this.primaryToken.decimals),
|
|
649
|
+
isReadyForNextSwap: swapAmounts[2] as boolean,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
getSettings = async (): Promise<YoloSettings> => {
|
|
654
|
+
const settings = await (await this.contract).call("get_settings", []);
|
|
655
|
+
return settings as YoloSettings;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
async getMaxTVL(): Promise<Web3Number> {
|
|
659
|
+
// Simply returning a fixed value for now, can automate later as per req
|
|
660
|
+
return new Web3Number("200000", 6);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async getUserPositionCards(input: UserPositionCardsInput): Promise<UserPositionCard[]> {
|
|
664
|
+
const userTVL = await this.getUserTVL(input.user);
|
|
665
|
+
const holdingsTitle = `${this.tokenForPrimaryPricing().symbol} Left`;
|
|
666
|
+
const earningsTitle = `${this.tokenForSecondaryPricing().symbol} Accumulated`;
|
|
667
|
+
const cards: UserPositionCard[] = [
|
|
668
|
+
{
|
|
669
|
+
title: "Your Holdings",
|
|
670
|
+
tooltip: "Combined value of your remaining base token and accumulated secondary token",
|
|
671
|
+
value: this.formatUSDForCard(userTVL.usdValue),
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
title: holdingsTitle,
|
|
675
|
+
tooltip: `Amount of ${this.tokenForPrimaryPricing().symbol} left in the vault, waiting to be swapped`,
|
|
676
|
+
value: this.formatTokenAmountForCard(userTVL.token0.amount, userTVL.token0.tokenInfo),
|
|
677
|
+
subValue: `≈ ${this.formatUSDForCard(userTVL.token0.usdValue)}`,
|
|
678
|
+
subValueColor: "positive",
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
title: earningsTitle,
|
|
682
|
+
tooltip: `Amount of ${this.tokenForSecondaryPricing().symbol} accumulated in the vault`,
|
|
683
|
+
value: this.formatTokenAmountForCard(userTVL.token1.amount, userTVL.token1.tokenInfo),
|
|
684
|
+
subValue: `≈ ${this.formatUSDForCard(userTVL.token1.usdValue)}`,
|
|
685
|
+
subValueColor: "default",
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
return cards;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const formatPriceLabel = (price: number): string => {
|
|
693
|
+
return `${price.toLocaleString("en-US")}`;
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const formatDurationSeconds = (seconds: number): string => {
|
|
697
|
+
return `${Math.floor(seconds / 3600).toLocaleString("en-US")} hours`;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const getLevelRangeLabel = (
|
|
701
|
+
level: YoloSpendingLevel,
|
|
702
|
+
secondaryTokenSymbol: string,
|
|
703
|
+
): string => {
|
|
704
|
+
if (level.minPrice !== undefined && level.maxPrice !== undefined) {
|
|
705
|
+
return `${formatPriceLabel(level.minPrice)}-${formatPriceLabel(level.maxPrice)} ${secondaryTokenSymbol}`;
|
|
706
|
+
}
|
|
707
|
+
if (level.minPrice !== undefined) {
|
|
708
|
+
return `>= ${formatPriceLabel(level.minPrice)} ${secondaryTokenSymbol}`;
|
|
709
|
+
}
|
|
710
|
+
return `< ${formatPriceLabel(level.maxPrice!)} ${secondaryTokenSymbol}`;
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const getSpendingLevelRows = (
|
|
714
|
+
levels: YoloSpendingLevel[],
|
|
715
|
+
secondaryTokenSymbol: string,
|
|
716
|
+
): Array<{ range: string; multiplier: string }> => {
|
|
717
|
+
const positiveSpends = levels.map((l) => l.spendPercent).filter((v) => v > 0);
|
|
718
|
+
const minSpendPercent = positiveSpends.length > 0 ? Math.min(...positiveSpends) : 1;
|
|
719
|
+
|
|
720
|
+
return levels.map((level) => {
|
|
721
|
+
const multiplier = level.spendPercent > 0 ? level.spendPercent / minSpendPercent : 0;
|
|
722
|
+
return {
|
|
723
|
+
range: getLevelRangeLabel(level, secondaryTokenSymbol),
|
|
724
|
+
multiplier: `${multiplier.toFixed(2)}x`,
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const getYoloVaultCopy = (input: YoloVaultSettings) => {
|
|
730
|
+
const main = input.mainToken.symbol;
|
|
731
|
+
const secondary = input.secondaryToken.symbol;
|
|
732
|
+
const spendingRows = getSpendingLevelRows(input.spendingLevels, secondary);
|
|
733
|
+
const description = createElement(
|
|
734
|
+
"div",
|
|
735
|
+
{
|
|
736
|
+
style: {
|
|
737
|
+
display: "flex",
|
|
738
|
+
flexDirection: "column",
|
|
739
|
+
gap: "12px",
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
createElement(
|
|
743
|
+
"p",
|
|
744
|
+
null,
|
|
745
|
+
`Troves Value Averaging (TVA) vault for ${main} -> ${secondary} accumulation.`
|
|
746
|
+
),
|
|
747
|
+
createElement(
|
|
748
|
+
"p",
|
|
749
|
+
null,
|
|
750
|
+
`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.`
|
|
751
|
+
),
|
|
752
|
+
createElement("p", null, createElement("strong", null, "Start Date: "), input.startDate),
|
|
753
|
+
createElement("p", null, createElement("strong", null, "Expiry Date: "), input.expiryDate),
|
|
754
|
+
createElement(
|
|
755
|
+
"p",
|
|
756
|
+
null,
|
|
757
|
+
createElement("strong", null, "Execution Window: "),
|
|
758
|
+
`${input.totalEpochs} epochs, minimum ${formatDurationSeconds(input.minEpochDurationSeconds)} per epoch`
|
|
759
|
+
),
|
|
760
|
+
createElement(
|
|
761
|
+
"p",
|
|
762
|
+
null,
|
|
763
|
+
createElement("strong", null, "TVA Edge vs DCA: "),
|
|
764
|
+
"deploys more aggressively into dips and stays measured when prices are high."
|
|
765
|
+
),
|
|
766
|
+
createElement("div", null, createElement("strong", null, "Spend Levels: ")),
|
|
767
|
+
createElement(
|
|
768
|
+
"div",
|
|
769
|
+
{
|
|
770
|
+
style: {
|
|
771
|
+
display: "grid",
|
|
772
|
+
gridTemplateColumns: "2fr 1fr",
|
|
773
|
+
gap: "4px 12px",
|
|
774
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
775
|
+
fontSize: "12px",
|
|
776
|
+
lineHeight: "18px",
|
|
777
|
+
padding: "8px 10px",
|
|
778
|
+
borderRadius: "8px",
|
|
779
|
+
background: "rgba(255,255,255,0.04)",
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
createElement("strong", { key: "h-range" }, "Range"),
|
|
783
|
+
createElement("strong", { key: "h-mult" }, "Multiplier"),
|
|
784
|
+
...spendingRows.flatMap((row, idx) => [
|
|
785
|
+
createElement("span", { key: `r-${idx}` }, row.range),
|
|
786
|
+
createElement("span", { key: `m-${idx}` }, row.multiplier),
|
|
787
|
+
])
|
|
788
|
+
),
|
|
789
|
+
createElement(
|
|
790
|
+
"p",
|
|
791
|
+
null,
|
|
792
|
+
"Learn the core ",
|
|
793
|
+
highlightTextWithLinks("value averaging", [
|
|
794
|
+
{
|
|
795
|
+
highlight: "value averaging",
|
|
796
|
+
link: "https://www.investopedia.com/terms/v/value_averaging.asp",
|
|
797
|
+
},
|
|
798
|
+
]),
|
|
799
|
+
" concept."
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
const vaultTypeDescription =
|
|
803
|
+
`Troves Value Averaging (TVA) vault to accumulate ${secondary} from ${main} using level-based buying until ${input.expiryDate}.`;
|
|
804
|
+
const faqs = [
|
|
805
|
+
{
|
|
806
|
+
question: `What is this ${secondary} TVA vault?`,
|
|
807
|
+
answer:
|
|
808
|
+
`You deposit ${main}, and troves executes epoch swaps to accumulate ${secondary} based on configured spend levels until ${input.expiryDate}.`,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
question: `Why TVA instead of standard DCA?`,
|
|
812
|
+
answer:
|
|
813
|
+
"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.",
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
question: `When does this vault expire (${input.expiryDate})?`,
|
|
817
|
+
answer:
|
|
818
|
+
`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.`,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
question: "How are spend levels applied?",
|
|
822
|
+
answer:
|
|
823
|
+
`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.`,
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
question: "How do withdrawals work?",
|
|
827
|
+
answer:
|
|
828
|
+
`Redemption returns your pro-rata remaining ${main} and accumulated ${secondary} based on shares.`,
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
question: "Is this strategy audited and open source?",
|
|
832
|
+
answer:
|
|
833
|
+
"Not yet. The strategy is currently not audited and the code is closed source.",
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
question: "What are the fees in this vault?",
|
|
837
|
+
answer:
|
|
838
|
+
"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.",
|
|
839
|
+
},
|
|
840
|
+
];
|
|
841
|
+
const investmentSteps = [
|
|
842
|
+
`Deposit ${main} after start (${input.startDate}) and before expiry (${input.expiryDate}).`,
|
|
843
|
+
`Each epoch (minimum ${formatDurationSeconds(input.minEpochDurationSeconds)}), troves swaps into ${secondary} based on the configured spending level.`,
|
|
844
|
+
`Lower prices can trigger higher spend percentages (TVA behavior).`,
|
|
845
|
+
`Redeem any time to receive your remaining ${main} plus accumulated ${secondary}.`,
|
|
846
|
+
`On expiry, entire ${main} token would have been swapped into ${secondary}, unless due to unfavourable market conditions.`
|
|
847
|
+
];
|
|
848
|
+
|
|
849
|
+
const parentTitle = secondary.replace('x', '');
|
|
850
|
+
const variantIntro = {
|
|
851
|
+
title: `${parentTitle} YOLO vault variant`,
|
|
852
|
+
description:
|
|
853
|
+
"Each variant runs until a different expiry date. TVA schedules and execution windows are configured per vault.",
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
title: `${parentTitle} YOLO (${input.expiryDate})`,
|
|
858
|
+
description,
|
|
859
|
+
vaultTypeDescription,
|
|
860
|
+
faqs,
|
|
861
|
+
investmentSteps,
|
|
862
|
+
parentTitle: `${parentTitle} YOLO`,
|
|
863
|
+
variantIntro,
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const usdc = Global.getDefaultTokens().find((t) => t.symbol === "USDC")!;
|
|
868
|
+
const wbtc = Global.getDefaultTokens().find((t) => t.symbol === "WBTC")!;
|
|
869
|
+
const xSTRK = Global.getDefaultTokens().find((t) => t.symbol === "xSTRK")!;
|
|
870
|
+
const xWBTC = Global.getDefaultTokens().find((t) => t.symbol === "xWBTC")!;
|
|
871
|
+
const vesuPrimeUSDC: TokenInfo = {
|
|
872
|
+
address: ContractAddr.from("0x00387e8ddbb1ab36ca08874d9abc702ef4872ad600dcf76b7f240b71d7bc4e65"),
|
|
873
|
+
symbol: "vUSDC",
|
|
874
|
+
name: "Vesu Prime USDC",
|
|
875
|
+
decimals: 18,
|
|
876
|
+
logo: usdc.logo,
|
|
877
|
+
displayDecimals: 2,
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const strk = Global.getDefaultTokens().find((t) => t.symbol === "STRK")!;
|
|
881
|
+
|
|
882
|
+
function getYoloVaultErc4626Config(mainToken: TokenInfo, secondaryToken: TokenInfo): {
|
|
883
|
+
isBaseERC4626: boolean;
|
|
884
|
+
isSecondERC4626: boolean;
|
|
885
|
+
baseUnderlying?: TokenInfo;
|
|
886
|
+
secondUnderlying?: TokenInfo;
|
|
887
|
+
} {
|
|
888
|
+
const isVesuPrime =
|
|
889
|
+
mainToken.address.address.toLowerCase() === vesuPrimeUSDC.address.address.toLowerCase();
|
|
890
|
+
if (isVesuPrime && secondaryToken.symbol.startsWith("x")) {
|
|
891
|
+
const secondUnderlying =
|
|
892
|
+
secondaryToken.symbol === "xWBTC"
|
|
893
|
+
? wbtc
|
|
894
|
+
: secondaryToken.symbol === "xSTRK"
|
|
895
|
+
? strk
|
|
896
|
+
: undefined;
|
|
897
|
+
if (!secondUnderlying) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`YOLO vault: unsupported x-token ${secondaryToken.symbol} for Vesu Prime USDC pair`,
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
isBaseERC4626: true,
|
|
904
|
+
isSecondERC4626: true,
|
|
905
|
+
baseUnderlying: usdc,
|
|
906
|
+
secondUnderlying,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
isBaseERC4626: false,
|
|
911
|
+
isSecondERC4626: false,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const vaultCommonProperties = {
|
|
916
|
+
totalEpochs: 1206407,
|
|
917
|
+
minEpochDurationSeconds: 21600,
|
|
918
|
+
feeBps: 50,
|
|
919
|
+
spendingLevels: [
|
|
920
|
+
{ minPrice: 80000, maxPrice: 100000, spendPercent: 50 },
|
|
921
|
+
{ minPrice: 70000, maxPrice: 80000, spendPercent: 70 },
|
|
922
|
+
{ minPrice: 60000, maxPrice: 70000, spendPercent: 100 },
|
|
923
|
+
{ minPrice: 50000, maxPrice: 60000, spendPercent: 250 },
|
|
924
|
+
{ maxPrice: 50000, spendPercent: 500 },
|
|
925
|
+
],
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const wbtc_parent = "wbtc-yolo";
|
|
929
|
+
const xstrk_parent = "xstrk-yolo";
|
|
930
|
+
|
|
931
|
+
const yoloVaultsConfig:YoloVaultStrategyConfig[] = [
|
|
932
|
+
{
|
|
933
|
+
address: ContractAddr.from("0x018ccdff25a642e211f86ace35ba282ebdf342330319ead98cae37258bc9cce1"),
|
|
934
|
+
mainToken: usdc,
|
|
935
|
+
secondaryToken: wbtc,
|
|
936
|
+
startDate: "03-MAR-2026",
|
|
937
|
+
expiryDate: "31-DEC-2026",
|
|
938
|
+
id: `btc-yolo-31-dec-2026`,
|
|
939
|
+
priority: 100,
|
|
940
|
+
...vaultCommonProperties,
|
|
941
|
+
parent_id: wbtc_parent,
|
|
942
|
+
},
|
|
943
|
+
// {
|
|
944
|
+
// address: ContractAddr.from("0x3381380c6cca17c2a20e1167a362d5b939e392311cbcdf2016f9c7c7a23a801"),
|
|
945
|
+
// mainToken: vesuPrimeUSDC,
|
|
946
|
+
// secondaryToken: xWBTC,
|
|
947
|
+
// startDate: "03-APR-2026",
|
|
948
|
+
// expiryDate: "31-MAY-2026",
|
|
949
|
+
// id: `vusdc-xwbct-yolo-31-may-2026`,
|
|
950
|
+
// priority: 100,
|
|
951
|
+
// ...vaultCommonProperties,
|
|
952
|
+
// parent_id: wbtc_parent
|
|
953
|
+
// },
|
|
954
|
+
// {
|
|
955
|
+
// address: ContractAddr.from("0x60c8466549a8e51eed0e8c38243fdb57d173c96cfdd4375b49ad1e338ff893"),
|
|
956
|
+
// mainToken: vesuPrimeUSDC,
|
|
957
|
+
// secondaryToken: xSTRK,
|
|
958
|
+
// startDate: "03-APR-2026",
|
|
959
|
+
// expiryDate: "31-MAR-2027",
|
|
960
|
+
// id: `xstrk-yolo-31-mar-2027`,
|
|
961
|
+
// priority: 50,
|
|
962
|
+
// ...vaultCommonProperties,
|
|
963
|
+
// spendingLevels: [
|
|
964
|
+
// { minPrice: 0.07, maxPrice: 0.1, spendPercent: 50 },
|
|
965
|
+
// { minPrice: 0.06, maxPrice: 0.07, spendPercent: 70 },
|
|
966
|
+
// { minPrice: 0.04, maxPrice: 0.06, spendPercent: 100 },
|
|
967
|
+
// { minPrice: 0.025, maxPrice: 0.04, spendPercent: 250 },
|
|
968
|
+
// { maxPrice: 0.025, spendPercent: 500 },
|
|
969
|
+
// ],
|
|
970
|
+
// parent_id: xstrk_parent
|
|
971
|
+
// },
|
|
972
|
+
// {
|
|
973
|
+
// address: ContractAddr.from("0x62499970196772c18ccf1da09910ece11d85d5df3e8f6d6e41b4d158fcb8e79"),
|
|
974
|
+
// mainToken: vesuPrimeUSDC,
|
|
975
|
+
// secondaryToken: xSTRK,
|
|
976
|
+
// startDate: "03-APR-2026",
|
|
977
|
+
// expiryDate: "30-JUN-2026",
|
|
978
|
+
// id: `xstrk-yolo-30-jun-2026`,
|
|
979
|
+
// priority: 50,
|
|
980
|
+
// ...vaultCommonProperties,
|
|
981
|
+
// spendingLevels: [
|
|
982
|
+
// { minPrice: 0.07, maxPrice: 0.1, spendPercent: 50 },
|
|
983
|
+
// { minPrice: 0.06, maxPrice: 0.07, spendPercent: 70 },
|
|
984
|
+
// { minPrice: 0.04, maxPrice: 0.06, spendPercent: 100 },
|
|
985
|
+
// { minPrice: 0.025, maxPrice: 0.04, spendPercent: 250 },
|
|
986
|
+
// { maxPrice: 0.025, spendPercent: 500 },
|
|
987
|
+
// ],
|
|
988
|
+
// parent_id: xstrk_parent
|
|
989
|
+
// }
|
|
990
|
+
]
|
|
991
|
+
|
|
992
|
+
// Risk factors intentionally left unrated for this strategy for now.
|
|
993
|
+
// const yoloRiskFactors: RiskFactor[] = [ ... ];
|
|
994
|
+
const yoloRiskFactors: RiskFactor[] = [];
|
|
995
|
+
|
|
996
|
+
export const YoloVaultStrategies: IStrategyMetadata<YoloVaultSettings>[] = yoloVaultsConfig.map(yoloConfig => {
|
|
997
|
+
const yoloCopy = getYoloVaultCopy(yoloConfig);
|
|
998
|
+
const erc4626Cfg = getYoloVaultErc4626Config(yoloConfig.mainToken, yoloConfig.secondaryToken);
|
|
999
|
+
return {
|
|
1000
|
+
id: yoloConfig.id,
|
|
1001
|
+
name: yoloCopy.title,
|
|
1002
|
+
parentName: yoloCopy.parentTitle,
|
|
1003
|
+
parentId: yoloConfig.parent_id,
|
|
1004
|
+
priority: yoloConfig.priority,
|
|
1005
|
+
variantIntro: yoloCopy.variantIntro,
|
|
1006
|
+
description: yoloCopy.description,
|
|
1007
|
+
address: yoloConfig.address,
|
|
1008
|
+
vaultType: {
|
|
1009
|
+
type: VaultType.TVA,
|
|
1010
|
+
description: yoloCopy.vaultTypeDescription,
|
|
1011
|
+
},
|
|
1012
|
+
curator: UnwrapLabsCurator,
|
|
1013
|
+
security: {
|
|
1014
|
+
auditStatus: AuditStatus.NOT_AUDITED,
|
|
1015
|
+
sourceCode: {
|
|
1016
|
+
type: SourceCodeType.CLOSED_SOURCE,
|
|
1017
|
+
contractLink: "",
|
|
1018
|
+
},
|
|
1019
|
+
accessControl: {
|
|
1020
|
+
type: AccessControlType.ROLE_BASED_ACCESS,
|
|
1021
|
+
addresses: [MY_ACCESS_CONTROL.address],
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
redemptionInfo: {
|
|
1025
|
+
instantWithdrawalVault: InstantWithdrawalVault.YES,
|
|
1026
|
+
redemptionsInfo: [],
|
|
1027
|
+
alerts: [],
|
|
1028
|
+
},
|
|
1029
|
+
usualTimeToEarnings: null,
|
|
1030
|
+
usualTimeToEarningsDescription: null,
|
|
1031
|
+
launchBlock: 0,
|
|
1032
|
+
type: "Other",
|
|
1033
|
+
depositTokens: [
|
|
1034
|
+
usdc,
|
|
1035
|
+
yoloConfig.secondaryToken,
|
|
1036
|
+
],
|
|
1037
|
+
protocols: [],
|
|
1038
|
+
risk: {
|
|
1039
|
+
riskFactor: yoloRiskFactors,
|
|
1040
|
+
netRisk: 0,
|
|
1041
|
+
notARisks: getNoRiskTags(yoloRiskFactors),
|
|
1042
|
+
},
|
|
1043
|
+
apyMethodology:
|
|
1044
|
+
"Not a primary yield strategy. Funds earn yield when idle, but the main return comes from BTC price appreciation and your conviction to hold. This vault simply helps you accumulate more BTC.",
|
|
1045
|
+
feeBps: {
|
|
1046
|
+
performanceFeeBps: 1000,
|
|
1047
|
+
},
|
|
1048
|
+
additionalInfo: {
|
|
1049
|
+
mainToken: yoloConfig.mainToken,
|
|
1050
|
+
secondaryToken: yoloConfig.secondaryToken,
|
|
1051
|
+
startDate: yoloConfig.startDate,
|
|
1052
|
+
expiryDate: yoloConfig.expiryDate,
|
|
1053
|
+
totalEpochs: yoloConfig.totalEpochs,
|
|
1054
|
+
minEpochDurationSeconds: yoloConfig.minEpochDurationSeconds,
|
|
1055
|
+
spendingLevels: yoloConfig.spendingLevels,
|
|
1056
|
+
feeBps: yoloConfig.feeBps, // swap fee bps
|
|
1057
|
+
isBaseERC4626: erc4626Cfg.isBaseERC4626,
|
|
1058
|
+
isSecondERC4626: erc4626Cfg.isSecondERC4626,
|
|
1059
|
+
...(erc4626Cfg.isBaseERC4626 && erc4626Cfg.baseUnderlying && erc4626Cfg.secondUnderlying
|
|
1060
|
+
? {
|
|
1061
|
+
baseUnderlying: erc4626Cfg.baseUnderlying,
|
|
1062
|
+
secondUnderlying: erc4626Cfg.secondUnderlying,
|
|
1063
|
+
}
|
|
1064
|
+
: {}),
|
|
1065
|
+
},
|
|
1066
|
+
faqs: yoloCopy.faqs,
|
|
1067
|
+
contractDetails: [{name: "Vault", address: yoloConfig.address}],
|
|
1068
|
+
investmentSteps: yoloCopy.investmentSteps,
|
|
1069
|
+
settings: {
|
|
1070
|
+
liveStatus: StrategyLiveStatus.HOT,
|
|
1071
|
+
isAudited: false,
|
|
1072
|
+
quoteToken:
|
|
1073
|
+
erc4626Cfg.isBaseERC4626 && erc4626Cfg.baseUnderlying
|
|
1074
|
+
? erc4626Cfg.baseUnderlying
|
|
1075
|
+
: yoloConfig.mainToken,
|
|
1076
|
+
isTransactionHistDisabled: true,
|
|
1077
|
+
},
|
|
1078
|
+
apyHistoryUIConfig: {
|
|
1079
|
+
showApyHistory: false,
|
|
1080
|
+
noApyHistoryMessage:
|
|
1081
|
+
"APY history is hidden because this is a TVA accumulation vault, not a yield-bearing APY strategy.",
|
|
1082
|
+
},
|
|
1083
|
+
}
|
|
1084
|
+
});
|