@strkfarm/sdk 1.0.55 → 1.0.57
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 +0 -0
- package/dist/cli.mjs +0 -0
- package/dist/index.browser.global.js +65617 -46434
- package/dist/index.browser.mjs +16547 -8981
- package/dist/index.d.ts +355 -15
- package/dist/index.js +19314 -11582
- package/dist/index.mjs +17620 -9895
- package/package.json +4 -3
- package/src/data/universal-vault.abi.json +1565 -0
- package/src/data/vault-manager.abi.json +634 -0
- package/src/data/vesu-singleton.abi.json +2247 -0
- package/src/dataTypes/address.ts +4 -0
- package/src/global.ts +30 -0
- package/src/interfaces/common.tsx +18 -1
- package/src/modules/pricer.ts +1 -1
- package/src/node/deployer.ts +219 -0
- package/src/node/index.ts +2 -1
- package/src/notifs/telegram.ts +0 -2
- package/src/strategies/base-strategy.ts +3 -23
- package/src/strategies/index.ts +3 -1
- package/src/strategies/sensei.ts +13 -6
- package/src/strategies/universal-adapters/adapter-utils.ts +13 -0
- package/src/strategies/universal-adapters/baseAdapter.ts +41 -0
- package/src/strategies/universal-adapters/common-adapter.ts +96 -0
- package/src/strategies/universal-adapters/index.ts +3 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +344 -0
- package/src/strategies/universal-strategy.ts +695 -0
- package/src/utils/cacheClass.ts +29 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/oz-merkle.ts +91 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
|
|
3
|
+
import { PricerBase } from "@/modules/pricerBase";
|
|
4
|
+
import { getNoRiskTags, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType, VaultPosition } from "@/interfaces";
|
|
5
|
+
import { Call, CallData, Contract, num, uint256 } from "starknet";
|
|
6
|
+
import { VesuRebalanceSettings } from "./vesu-rebalance";
|
|
7
|
+
import { assert, LeafData, logger, StandardMerkleTree } from "@/utils";
|
|
8
|
+
import UniversalVaultAbi from '../data/universal-vault.abi.json';
|
|
9
|
+
import ManagerAbi from '../data/vault-manager.abi.json';
|
|
10
|
+
import { ApproveCallParams, BaseAdapter, CommonAdapter, FlashloanCallParams, GenerateCallFn, LeafAdapterFn, ManageCall, VesuAdapter, VesuModifyPositionCallParams, VesuPools } from "./universal-adapters";
|
|
11
|
+
import { Global } from "@/global";
|
|
12
|
+
|
|
13
|
+
export interface UniversalStrategySettings {
|
|
14
|
+
manager: ContractAddr,
|
|
15
|
+
vaultAllocator: ContractAddr,
|
|
16
|
+
redeemRequestNFT: ContractAddr,
|
|
17
|
+
leafAdapters: LeafAdapterFn<any>[],
|
|
18
|
+
adapters: {id: string, adapter: BaseAdapter}[],
|
|
19
|
+
targetHealthFactor: number,
|
|
20
|
+
minHealthFactor: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export class UniversalStrategy<
|
|
25
|
+
S extends UniversalStrategySettings
|
|
26
|
+
> extends BaseStrategy<
|
|
27
|
+
SingleTokenInfo,
|
|
28
|
+
SingleActionAmount
|
|
29
|
+
> {
|
|
30
|
+
|
|
31
|
+
/** Contract address of the strategy */
|
|
32
|
+
readonly address: ContractAddr;
|
|
33
|
+
/** Pricer instance for token price calculations */
|
|
34
|
+
readonly pricer: PricerBase;
|
|
35
|
+
/** Metadata containing strategy information */
|
|
36
|
+
readonly metadata: IStrategyMetadata<S>;
|
|
37
|
+
/** Contract instance for interacting with the strategy */
|
|
38
|
+
readonly contract: Contract;
|
|
39
|
+
readonly managerContract: Contract;
|
|
40
|
+
merkleTree: StandardMerkleTree | undefined;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
config: IConfig,
|
|
44
|
+
pricer: PricerBase,
|
|
45
|
+
metadata: IStrategyMetadata<S>
|
|
46
|
+
) {
|
|
47
|
+
super(config);
|
|
48
|
+
this.pricer = pricer;
|
|
49
|
+
|
|
50
|
+
assert(
|
|
51
|
+
metadata.depositTokens.length === 1,
|
|
52
|
+
"VesuRebalance only supports 1 deposit token"
|
|
53
|
+
);
|
|
54
|
+
this.metadata = metadata;
|
|
55
|
+
this.address = metadata.address;
|
|
56
|
+
|
|
57
|
+
this.contract = new Contract(
|
|
58
|
+
UniversalVaultAbi,
|
|
59
|
+
this.address.address,
|
|
60
|
+
this.config.provider
|
|
61
|
+
);
|
|
62
|
+
this.managerContract = new Contract(
|
|
63
|
+
ManagerAbi,
|
|
64
|
+
this.metadata.additionalInfo.manager.address,
|
|
65
|
+
this.config.provider
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getMerkleTree() {
|
|
70
|
+
if (this.merkleTree) return this.merkleTree;
|
|
71
|
+
const leaves = this.metadata.additionalInfo.leafAdapters.map((adapter, index) => {
|
|
72
|
+
return adapter()
|
|
73
|
+
});
|
|
74
|
+
const standardTree = StandardMerkleTree.of(leaves.map(l => l.leaf));
|
|
75
|
+
this.merkleTree = standardTree;
|
|
76
|
+
return standardTree;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getMerkleRoot() {
|
|
80
|
+
return this.getMerkleTree().root;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getProofs<T>(id: string): { proofs: string[], callConstructor: GenerateCallFn<T> } {
|
|
84
|
+
const tree = this.getMerkleTree();
|
|
85
|
+
let proofs: string[] = [];
|
|
86
|
+
for (const [i, v] of tree.entries()) {
|
|
87
|
+
if (v.readableId == id) {
|
|
88
|
+
proofs = tree.getProof(i);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (proofs.length === 0) {
|
|
92
|
+
throw new Error(`Proof not found for ID: ${id}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// find leaf adapter
|
|
96
|
+
const leafAdapter = this.metadata.additionalInfo.leafAdapters.find(adapter => adapter().leaf.readableId === id);
|
|
97
|
+
if (!leafAdapter) {
|
|
98
|
+
throw new Error(`Leaf adapter not found for ID: ${id}`);
|
|
99
|
+
}
|
|
100
|
+
const leafInfo = leafAdapter();
|
|
101
|
+
return {
|
|
102
|
+
proofs,
|
|
103
|
+
callConstructor: leafInfo.callConstructor.bind(leafInfo),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getAdapter(id: string): BaseAdapter {
|
|
108
|
+
const adapter = this.metadata.additionalInfo.adapters.find(adapter => adapter.id === id);
|
|
109
|
+
if (!adapter) {
|
|
110
|
+
throw new Error(`Adapter not found for ID: ${id}`);
|
|
111
|
+
}
|
|
112
|
+
return adapter.adapter;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
asset() {
|
|
116
|
+
return this.metadata.depositTokens[0];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr): Promise<Call[]> {
|
|
120
|
+
// Technically its not erc4626 abi, but we just need approve call
|
|
121
|
+
// so, its ok to use it
|
|
122
|
+
assert(
|
|
123
|
+
amountInfo.tokenInfo.address.eq(this.asset().address),
|
|
124
|
+
"Deposit token mismatch"
|
|
125
|
+
);
|
|
126
|
+
const assetContract = new Contract(
|
|
127
|
+
UniversalVaultAbi,
|
|
128
|
+
this.asset().address.address,
|
|
129
|
+
this.config.provider
|
|
130
|
+
);
|
|
131
|
+
const call1 = assetContract.populate("approve", [
|
|
132
|
+
this.address.address,
|
|
133
|
+
uint256.bnToUint256(amountInfo.amount.toWei())
|
|
134
|
+
]);
|
|
135
|
+
const call2 = this.contract.populate("deposit", [
|
|
136
|
+
uint256.bnToUint256(amountInfo.amount.toWei()),
|
|
137
|
+
receiver.address
|
|
138
|
+
]);
|
|
139
|
+
return [call1, call2];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculates the Total Value Locked (TVL) for a specific user.
|
|
144
|
+
* @param user - Address of the user
|
|
145
|
+
* @returns Object containing the amount in token units and USD value
|
|
146
|
+
*/
|
|
147
|
+
async getUserTVL(user: ContractAddr) {
|
|
148
|
+
const shares = await this.contract.balanceOf(user.address);
|
|
149
|
+
const assets = await this.contract.convert_to_assets(
|
|
150
|
+
uint256.bnToUint256(shares)
|
|
151
|
+
);
|
|
152
|
+
const amount = Web3Number.fromWei(
|
|
153
|
+
assets.toString(),
|
|
154
|
+
this.metadata.depositTokens[0].decimals
|
|
155
|
+
);
|
|
156
|
+
let price = await this.pricer.getPrice(
|
|
157
|
+
this.metadata.depositTokens[0].symbol
|
|
158
|
+
);
|
|
159
|
+
const usdValue = Number(amount.toFixed(6)) * price.price;
|
|
160
|
+
return {
|
|
161
|
+
tokenInfo: this.asset(),
|
|
162
|
+
amount,
|
|
163
|
+
usdValue
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Calculates the weighted average APY across all pools based on USD value.
|
|
169
|
+
* @returns {Promise<number>} The weighted average APY across all pools
|
|
170
|
+
*/
|
|
171
|
+
async netAPY(): Promise<{ net: number, splits: { apy: number, id: string }[] }> {
|
|
172
|
+
const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
|
|
173
|
+
const pools = await VesuAdapter.getVesuPools();
|
|
174
|
+
const pool1 = pools.pools.find(p => vesuAdapter1.config.poolId.eqString(num.getHexString(p.id)));
|
|
175
|
+
const pool2 = pools.pools.find(p => vesuAdapter2.config.poolId.eqString(num.getHexString(p.id)));
|
|
176
|
+
if (!pool1 || !pool2) {
|
|
177
|
+
throw new Error('Pool not found');
|
|
178
|
+
};
|
|
179
|
+
const collateralAsset1 = pool1.assets.find((a: any) => a.symbol === vesuAdapter1.config.collateral.symbol)?.stats!;
|
|
180
|
+
const debtAsset1 = pool1.assets.find((a: any) => a.symbol === vesuAdapter1.config.debt.symbol)?.stats!;
|
|
181
|
+
const collateralAsset2 = pool2.assets.find((a: any) => a.symbol === vesuAdapter2.config.collateral.symbol)?.stats!;
|
|
182
|
+
const debtAsset2 = pool2.assets.find((a: any) => a.symbol === vesuAdapter2.config.debt.symbol)?.stats!;
|
|
183
|
+
|
|
184
|
+
// supplyApy: { value: '8057256029163289', decimals: 18 },
|
|
185
|
+
// defiSpringSupplyApr: { value: '46856062629264560', decimals: 18 },
|
|
186
|
+
// borrowApr: { value: '12167825982336000', decimals: 18 },
|
|
187
|
+
const collateral1APY = Number(collateralAsset1.supplyApy.value) / 1e18;
|
|
188
|
+
const debt1APY = Number(debtAsset1.borrowApr.value) / 1e18;
|
|
189
|
+
const collateral2APY = Number(collateralAsset2.supplyApy.value) / 1e18;
|
|
190
|
+
const debt2APY = Number(debtAsset2.borrowApr.value) / 1e18;
|
|
191
|
+
|
|
192
|
+
const positions = await this.getVaultPositions();
|
|
193
|
+
const weights = positions.map((p, index) => p.usdValue * (index % 2 == 0 ? 1 : -1));
|
|
194
|
+
const baseAPYs = [collateral1APY, debt1APY, collateral2APY, debt2APY];
|
|
195
|
+
assert(positions.length == baseAPYs.length, "Positions and APYs length mismatch");
|
|
196
|
+
const rewardAPYs = [Number(collateralAsset1.defiSpringSupplyApr.value) / 1e18, 0, Number(collateralAsset2.defiSpringSupplyApr.value) / 1e18, 0];
|
|
197
|
+
const baseAPY = this.computeAPY(baseAPYs, weights);
|
|
198
|
+
const rewardAPY = this.computeAPY(rewardAPYs, weights);
|
|
199
|
+
const apys = [...baseAPYs, ...rewardAPYs];
|
|
200
|
+
const netAPY = baseAPY + rewardAPY;
|
|
201
|
+
return { net: netAPY, splits: [{
|
|
202
|
+
apy: baseAPY, id: 'base'
|
|
203
|
+
}, {
|
|
204
|
+
apy: rewardAPY, id: 'defispring'
|
|
205
|
+
}] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private computeAPY(apys: number[], weights: number[]) {
|
|
209
|
+
assert(apys.length === weights.length, "APYs and weights length mismatch");
|
|
210
|
+
const weightedSum = apys.reduce((acc, apy, i) => acc + apy * weights[i], 0);
|
|
211
|
+
const totalWeight = weights.reduce((acc, weight) => acc + weight, 0);
|
|
212
|
+
return weightedSum / totalWeight;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Calculates the total TVL of the strategy.
|
|
217
|
+
* @returns Object containing the total amount in token units and USD value
|
|
218
|
+
*/
|
|
219
|
+
async getTVL() {
|
|
220
|
+
const assets = await this.contract.total_assets();
|
|
221
|
+
const amount = Web3Number.fromWei(
|
|
222
|
+
assets.toString(),
|
|
223
|
+
this.metadata.depositTokens[0].decimals
|
|
224
|
+
);
|
|
225
|
+
let price = await this.pricer.getPrice(
|
|
226
|
+
this.metadata.depositTokens[0].symbol
|
|
227
|
+
);
|
|
228
|
+
const usdValue = Number(amount.toFixed(6)) * price.price;
|
|
229
|
+
return {
|
|
230
|
+
tokenInfo: this.asset(),
|
|
231
|
+
amount,
|
|
232
|
+
usdValue
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async getAUM(): Promise<{net: SingleTokenInfo, prevAum: Web3Number, splits: {id: string, aum: Web3Number}[]}> {
|
|
237
|
+
const currentAUM: bigint = await this.contract.call('aum', []) as bigint;
|
|
238
|
+
const lastReportTime = await this.contract.call('last_report_timestamp', []);
|
|
239
|
+
|
|
240
|
+
const token1Price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
|
|
241
|
+
|
|
242
|
+
// calculate actual aum
|
|
243
|
+
const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
|
|
244
|
+
const leg1AUM = await vesuAdapter1.getPositions(this.config);
|
|
245
|
+
const leg2AUM = await vesuAdapter2.getPositions(this.config);
|
|
246
|
+
|
|
247
|
+
const aumToken = leg1AUM[0].amount
|
|
248
|
+
.plus(leg2AUM[0].usdValue / token1Price.price)
|
|
249
|
+
.minus(leg1AUM[1].usdValue / token1Price.price)
|
|
250
|
+
.minus(leg2AUM[1].amount);
|
|
251
|
+
logger.verbose(`${this.getTag()} Actual AUM: ${aumToken}`);
|
|
252
|
+
|
|
253
|
+
// calculate estimated growth from strk rewards
|
|
254
|
+
const netAPY = await this.netAPY();
|
|
255
|
+
const defispringAPY = netAPY.splits.find(s => s.id === 'defispring')?.apy || 0;
|
|
256
|
+
if (!defispringAPY) throw new Error('DefiSpring APY not found');
|
|
257
|
+
|
|
258
|
+
const timeDiff = (Math.round(Date.now() / 1000) - Number(lastReportTime));
|
|
259
|
+
const growthRate = timeDiff * defispringAPY / (365 * 24 * 60 * 60);
|
|
260
|
+
const prevAum = Web3Number.fromWei(currentAUM.toString(), this.asset().decimals);
|
|
261
|
+
const rewardAssets = prevAum.multipliedBy(growthRate);
|
|
262
|
+
logger.verbose(`${this.getTag()} DefiSpring AUM time difference: ${timeDiff}`);
|
|
263
|
+
logger.verbose(`${this.getTag()} Current AUM: ${currentAUM}`);
|
|
264
|
+
logger.verbose(`${this.getTag()} Net APY: ${JSON.stringify(netAPY)}`);
|
|
265
|
+
logger.verbose(`${this.getTag()} rewards AUM: ${rewardAssets}`);
|
|
266
|
+
|
|
267
|
+
const newAUM = aumToken.plus(rewardAssets);
|
|
268
|
+
logger.verbose(`${this.getTag()} New AUM: ${newAUM}`);
|
|
269
|
+
|
|
270
|
+
const net = {
|
|
271
|
+
tokenInfo: this.asset(),
|
|
272
|
+
amount: newAUM,
|
|
273
|
+
usdValue: newAUM.multipliedBy(token1Price.price).toNumber()
|
|
274
|
+
};
|
|
275
|
+
const splits = [{
|
|
276
|
+
id: 'finalised',
|
|
277
|
+
aum: aumToken
|
|
278
|
+
}, {
|
|
279
|
+
id: 'defispring',
|
|
280
|
+
aum: rewardAssets
|
|
281
|
+
}];
|
|
282
|
+
return { net, splits, prevAum };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getVesuAdapters() {
|
|
286
|
+
const vesuAdapter1 = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG1) as VesuAdapter;
|
|
287
|
+
const vesuAdapter2 = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG2) as VesuAdapter;
|
|
288
|
+
vesuAdapter1.pricer = this.pricer;
|
|
289
|
+
vesuAdapter2.pricer = this.pricer;
|
|
290
|
+
vesuAdapter1.networkConfig = this.config;
|
|
291
|
+
vesuAdapter2.networkConfig = this.config;
|
|
292
|
+
|
|
293
|
+
return [vesuAdapter1, vesuAdapter2];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async getVaultPositions(): Promise<VaultPosition[]> {
|
|
297
|
+
const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
|
|
298
|
+
const leg1Positions = await vesuAdapter1.getPositions(this.config);
|
|
299
|
+
const leg2Positions = await vesuAdapter2.getPositions(this.config);
|
|
300
|
+
return [...leg1Positions, ...leg2Positions];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getSetManagerCall(strategist: ContractAddr, root = this.getMerkleRoot()) {
|
|
304
|
+
return this.managerContract.populate('set_manage_root', [strategist.address, num.getHexString(root)]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
getManageCall(proofIds: string[], manageCalls: ManageCall[]) {
|
|
308
|
+
assert(proofIds.length == manageCalls.length, 'Proof IDs and Manage Calls length mismatch');
|
|
309
|
+
return this.managerContract.populate('manage_vault_with_merkle_verification', {
|
|
310
|
+
proofs: proofIds.map(id => this.getProofs(id).proofs),
|
|
311
|
+
decoder_and_sanitizers: manageCalls.map(call => call.sanitizer.address),
|
|
312
|
+
targets: manageCalls.map(call => call.call.contractAddress.address),
|
|
313
|
+
selectors: manageCalls.map(call => call.call.selector),
|
|
314
|
+
calldatas: manageCalls.map(call => call.call.calldata), // Calldata[]
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getVesuModifyPositionCalls(params: {
|
|
319
|
+
isLeg1: boolean,
|
|
320
|
+
isDeposit: boolean,
|
|
321
|
+
depositAmount: Web3Number,
|
|
322
|
+
debtAmount: Web3Number
|
|
323
|
+
}) {
|
|
324
|
+
assert(params.depositAmount.gt(0) || params.debtAmount.gt(0), 'Either deposit or debt amount must be greater than 0');
|
|
325
|
+
// approve token
|
|
326
|
+
const isToken1 = params.isLeg1 == params.isDeposit; // XOR
|
|
327
|
+
const STEP1_ID = isToken1 ? UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1 :UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN2;
|
|
328
|
+
const manage4Info = this.getProofs<ApproveCallParams>(STEP1_ID);
|
|
329
|
+
const approveAmount = params.isDeposit ? params.depositAmount : params.debtAmount;
|
|
330
|
+
const manageCall4 = manage4Info.callConstructor({
|
|
331
|
+
amount: approveAmount
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// deposit and borrow or repay and withdraw
|
|
335
|
+
const STEP2_ID = params.isLeg1 ? UNIVERSAL_MANAGE_IDS.VESU_LEG1 : UNIVERSAL_MANAGE_IDS.VESU_LEG2;
|
|
336
|
+
const manage5Info = this.getProofs<VesuModifyPositionCallParams>(STEP2_ID);
|
|
337
|
+
const manageCall5 = manage5Info.callConstructor(VesuAdapter.getDefaultModifyPositionCallParams({
|
|
338
|
+
collateralAmount: params.depositAmount,
|
|
339
|
+
isAddCollateral: params.isDeposit,
|
|
340
|
+
debtAmount: params.debtAmount,
|
|
341
|
+
isBorrow: params.isDeposit
|
|
342
|
+
}))
|
|
343
|
+
|
|
344
|
+
const output = [{
|
|
345
|
+
proofs: manage5Info.proofs,
|
|
346
|
+
manageCall: manageCall5,
|
|
347
|
+
step: STEP2_ID
|
|
348
|
+
}];
|
|
349
|
+
if (approveAmount.gt(0)) {
|
|
350
|
+
output.unshift({
|
|
351
|
+
proofs: manage4Info.proofs,
|
|
352
|
+
manageCall: manageCall4,
|
|
353
|
+
step: STEP1_ID
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
return output;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getTag() {
|
|
360
|
+
return `${UniversalStrategy.name}:${this.metadata.name}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async getVesuHealthFactors() {
|
|
364
|
+
return await Promise.all(this.getVesuAdapters().map(v => v.getHealthFactor()));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async computeRebalanceConditionAndReturnCalls(): Promise<Call[]> {
|
|
368
|
+
const vesuAdapters = this.getVesuAdapters();
|
|
369
|
+
const healthFactors = await this.getVesuHealthFactors();
|
|
370
|
+
const leg1HealthFactor = healthFactors[0];
|
|
371
|
+
const leg2HealthFactor = healthFactors[1];
|
|
372
|
+
logger.verbose(`${this.getTag()}: HealthFactorLeg1: ${leg1HealthFactor}`);
|
|
373
|
+
logger.verbose(`${this.getTag()}: HealthFactorLeg2: ${leg2HealthFactor}`);
|
|
374
|
+
|
|
375
|
+
const minHf = this.metadata.additionalInfo.minHealthFactor;
|
|
376
|
+
const isRebalanceNeeded1 = leg1HealthFactor < minHf;
|
|
377
|
+
const isRebalanceNeeded2 = leg2HealthFactor < minHf;
|
|
378
|
+
if (!isRebalanceNeeded1 && !isRebalanceNeeded2) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (isRebalanceNeeded1) {
|
|
383
|
+
const amount = await this.getLegRebalanceAmount(vesuAdapters[0], leg1HealthFactor, false);
|
|
384
|
+
const leg2HF = await this.getNewHealthFactor(vesuAdapters[1], amount, true);
|
|
385
|
+
assert(leg2HF > minHf, `Rebalance Leg1 failed: Leg2 HF after rebalance would be too low: ${leg2HF}`);
|
|
386
|
+
return [await this.getRebalanceCall({
|
|
387
|
+
isLeg1toLeg2: false,
|
|
388
|
+
amount: amount
|
|
389
|
+
})];
|
|
390
|
+
} else {
|
|
391
|
+
const amount = await this.getLegRebalanceAmount(vesuAdapters[1], leg2HealthFactor, true);
|
|
392
|
+
const leg1HF = await this.getNewHealthFactor(vesuAdapters[0], amount, false);
|
|
393
|
+
assert(leg1HF > minHf, `Rebalance Leg2 failed: Leg1 HF after rebalance would be too low: ${leg1HF}`);
|
|
394
|
+
return [await this.getRebalanceCall({
|
|
395
|
+
isLeg1toLeg2: true,
|
|
396
|
+
amount: amount
|
|
397
|
+
})];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async getNewHealthFactor(vesuAdapter: VesuAdapter, newAmount: Web3Number, isWithdraw: boolean) {
|
|
402
|
+
const {
|
|
403
|
+
collateralTokenAmount,
|
|
404
|
+
collateralUSDAmount,
|
|
405
|
+
collateralPrice,
|
|
406
|
+
debtTokenAmount,
|
|
407
|
+
debtUSDAmount,
|
|
408
|
+
debtPrice,
|
|
409
|
+
ltv
|
|
410
|
+
} = await vesuAdapter.getAssetPrices();
|
|
411
|
+
|
|
412
|
+
if (isWithdraw) {
|
|
413
|
+
const newHF = ((collateralTokenAmount.toNumber() - newAmount.toNumber()) * collateralPrice * ltv) / debtUSDAmount;
|
|
414
|
+
logger.verbose(`getNewHealthFactor:: HF: ${newHF}, amoutn: ${newAmount.toNumber()}, isDeposit`);
|
|
415
|
+
return newHF;
|
|
416
|
+
} else { // is borrow
|
|
417
|
+
const newHF = (collateralUSDAmount * ltv) / ((debtTokenAmount.toNumber() + newAmount.toNumber()) * debtPrice);
|
|
418
|
+
logger.verbose(`getNewHealthFactor:: HF: ${newHF}, amoutn: ${newAmount.toNumber()}, isRepay`);
|
|
419
|
+
return newHF;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
*
|
|
425
|
+
* @param vesuAdapter
|
|
426
|
+
* @param currentHf
|
|
427
|
+
* @param isDeposit if true, attempt by adding collateral, else by repaying
|
|
428
|
+
* @returns
|
|
429
|
+
*/
|
|
430
|
+
private async getLegRebalanceAmount(vesuAdapter: VesuAdapter, currentHf: number, isDeposit: boolean) {
|
|
431
|
+
const {
|
|
432
|
+
collateralTokenAmount,
|
|
433
|
+
collateralUSDAmount,
|
|
434
|
+
collateralPrice,
|
|
435
|
+
debtTokenAmount,
|
|
436
|
+
debtUSDAmount,
|
|
437
|
+
debtPrice,
|
|
438
|
+
ltv
|
|
439
|
+
} = await vesuAdapter.getAssetPrices();
|
|
440
|
+
|
|
441
|
+
// debt is zero, nothing to rebalance
|
|
442
|
+
if(debtTokenAmount.isZero()) {
|
|
443
|
+
return Web3Number.fromWei(0, 0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
assert(collateralPrice > 0 && debtPrice > 0, "getRebalanceAmount: Invalid price");
|
|
447
|
+
|
|
448
|
+
// avoid calculating for too close
|
|
449
|
+
const targetHF = this.metadata.additionalInfo.targetHealthFactor;
|
|
450
|
+
if (currentHf > targetHF - 0.01)
|
|
451
|
+
throw new Error("getLegRebalanceAmount: Current health factor is healthy");
|
|
452
|
+
|
|
453
|
+
if (isDeposit) {
|
|
454
|
+
// TargetHF = (collAmount + newAmount) * price * ltv / debtUSD
|
|
455
|
+
const newAmount = targetHF * debtUSDAmount / (collateralPrice * ltv) - collateralTokenAmount.toNumber();
|
|
456
|
+
logger.verbose(`${this.getTag()}:: getLegRebalanceAmount: addCollateral, currentHf: ${currentHf}, targetHF: ${targetHF}, collAmount: ${collateralTokenAmount.toString()}, collUSD: ${collateralUSDAmount}, collPrice: ${collateralPrice}, debtAmount: ${debtTokenAmount.toString()}, debtUSD: ${debtUSDAmount}, debtPrice: ${debtPrice}, ltv: ${ltv}, newAmount: ${newAmount}`);
|
|
457
|
+
return new Web3Number(newAmount.toFixed(8), collateralTokenAmount.decimals);
|
|
458
|
+
} else {
|
|
459
|
+
// TargetHF = collUSD * ltv / (debtAmount - newAmount) * debtPrice
|
|
460
|
+
const newAmount = debtTokenAmount.toNumber() - collateralUSDAmount * ltv / (targetHF * debtPrice);
|
|
461
|
+
logger.verbose(`${this.getTag()}:: getLegRebalanceAmount: repayDebt, currentHf: ${currentHf}, targetHF: ${targetHF}, collAmount: ${collateralTokenAmount.toString()}, collUSD: ${collateralUSDAmount}, collPrice: ${collateralPrice}, debtAmount: ${debtTokenAmount.toString()}, debtUSD: ${debtUSDAmount}, debtPrice: ${debtPrice}, ltv: ${ltv}, newAmount: ${newAmount}`);
|
|
462
|
+
return new Web3Number(newAmount.toFixed(8), debtTokenAmount.decimals);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async getVesuMultiplyCall(params: {
|
|
467
|
+
isDeposit: boolean,
|
|
468
|
+
leg1DepositAmount: Web3Number
|
|
469
|
+
}) {
|
|
470
|
+
const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
|
|
471
|
+
const leg1LTV = await vesuAdapter1.getLTVConfig(this.config);
|
|
472
|
+
const leg2LTV = await vesuAdapter2.getLTVConfig(this.config);
|
|
473
|
+
logger.verbose(`${this.getTag()}: LTVLeg1: ${leg1LTV}`);
|
|
474
|
+
logger.verbose(`${this.getTag()}: LTVLeg2: ${leg2LTV}`);
|
|
475
|
+
|
|
476
|
+
const token1Price = await this.pricer.getPrice(vesuAdapter1.config.collateral.symbol);
|
|
477
|
+
const token2Price = await this.pricer.getPrice(vesuAdapter2.config.collateral.symbol);
|
|
478
|
+
logger.verbose(`${this.getTag()}: Price${vesuAdapter1.config.collateral.symbol}: ${token1Price.price}`);
|
|
479
|
+
logger.verbose(`${this.getTag()}: Price${vesuAdapter2.config.collateral.symbol}: ${token2Price.price}`);
|
|
480
|
+
|
|
481
|
+
const TARGET_HF = this.metadata.additionalInfo.targetHealthFactor;
|
|
482
|
+
|
|
483
|
+
const k1 = token1Price.price * leg1LTV / token2Price.price / TARGET_HF;
|
|
484
|
+
const k2 = token1Price.price * TARGET_HF / token2Price.price / leg2LTV;
|
|
485
|
+
|
|
486
|
+
const borrow2Amount = new Web3Number(
|
|
487
|
+
params.leg1DepositAmount.multipliedBy(k1.toFixed(6)).dividedBy(k2 - k1).toFixed(6),
|
|
488
|
+
vesuAdapter2.config.debt.decimals
|
|
489
|
+
);
|
|
490
|
+
const borrow1Amount = new Web3Number(
|
|
491
|
+
borrow2Amount.multipliedBy(k2).toFixed(6),
|
|
492
|
+
vesuAdapter1.config.debt.decimals
|
|
493
|
+
);
|
|
494
|
+
logger.verbose(`${this.getTag()}:: leg1DepositAmount: ${params.leg1DepositAmount.toString()} ${vesuAdapter1.config.collateral.symbol}`);
|
|
495
|
+
logger.verbose(`${this.getTag()}:: borrow1Amount: ${borrow1Amount.toString()} ${vesuAdapter1.config.debt.symbol}`);
|
|
496
|
+
logger.verbose(`${this.getTag()}:: borrow2Amount: ${borrow2Amount.toString()} ${vesuAdapter2.config.debt.symbol}`);
|
|
497
|
+
|
|
498
|
+
const callSet1 = this.getVesuModifyPositionCalls({
|
|
499
|
+
isLeg1: true,
|
|
500
|
+
isDeposit: params.isDeposit,
|
|
501
|
+
depositAmount: params.leg1DepositAmount.plus(borrow2Amount),
|
|
502
|
+
debtAmount: borrow1Amount
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const callSet2 = this.getVesuModifyPositionCalls({
|
|
506
|
+
isLeg1: false,
|
|
507
|
+
isDeposit: params.isDeposit,
|
|
508
|
+
depositAmount: borrow1Amount,
|
|
509
|
+
debtAmount: borrow2Amount
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const allActions = [...callSet1.map(i => i.manageCall), ...callSet2.map(i => i.manageCall)];
|
|
513
|
+
const flashloanCalldata = CallData.compile([
|
|
514
|
+
[...callSet1.map(i => i.proofs), ...callSet2.map(i => i.proofs)],
|
|
515
|
+
allActions.map(i => i.sanitizer.address),
|
|
516
|
+
allActions.map(i => i.call.contractAddress.address),
|
|
517
|
+
allActions.map(i => i.call.selector),
|
|
518
|
+
allActions.map(i => i.call.calldata)
|
|
519
|
+
])
|
|
520
|
+
|
|
521
|
+
// flash loan
|
|
522
|
+
const STEP1_ID = UNIVERSAL_MANAGE_IDS.FLASH_LOAN;
|
|
523
|
+
const manage1Info = this.getProofs<FlashloanCallParams>(STEP1_ID);
|
|
524
|
+
const manageCall1 = manage1Info.callConstructor({
|
|
525
|
+
amount: borrow2Amount,
|
|
526
|
+
data: flashloanCalldata.map(i => BigInt(i))
|
|
527
|
+
})
|
|
528
|
+
const manageCall = this.getManageCall([UNIVERSAL_MANAGE_IDS.FLASH_LOAN], [manageCall1]);
|
|
529
|
+
return manageCall;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async getRebalanceCall(params: {
|
|
533
|
+
isLeg1toLeg2: boolean,
|
|
534
|
+
amount: Web3Number
|
|
535
|
+
}) {
|
|
536
|
+
let callSet1 = this.getVesuModifyPositionCalls({
|
|
537
|
+
isLeg1: true,
|
|
538
|
+
isDeposit: params.isLeg1toLeg2,
|
|
539
|
+
depositAmount: Web3Number.fromWei(0, 0),
|
|
540
|
+
debtAmount: params.amount
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
let callSet2 = this.getVesuModifyPositionCalls({
|
|
544
|
+
isLeg1: false,
|
|
545
|
+
isDeposit: params.isLeg1toLeg2,
|
|
546
|
+
depositAmount: params.amount,
|
|
547
|
+
debtAmount: Web3Number.fromWei(0, 0)
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (params.isLeg1toLeg2) {
|
|
551
|
+
const manageCall = this.getManageCall([
|
|
552
|
+
...callSet1.map(i => i.step), ...callSet2.map(i => i.step)
|
|
553
|
+
], [...callSet1.map(i => i.manageCall), ...callSet2.map(i => i.manageCall)]);
|
|
554
|
+
return manageCall;
|
|
555
|
+
} else {
|
|
556
|
+
const manageCall = this.getManageCall([
|
|
557
|
+
...callSet2.map(i => i.step), ...callSet1.map(i => i.step)
|
|
558
|
+
], [...callSet2.map(i => i.manageCall), ...callSet1.map(i => i.manageCall)]);
|
|
559
|
+
return manageCall;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
export enum UNIVERSAL_MANAGE_IDS {
|
|
566
|
+
FLASH_LOAN = 'flash_loan_init',
|
|
567
|
+
VESU_LEG1 = 'vesu_leg1',
|
|
568
|
+
VESU_LEG2 = 'vesu_leg2',
|
|
569
|
+
APPROVE_TOKEN1 = 'approve_token1',
|
|
570
|
+
APPROVE_TOKEN2 = 'approve_token2'
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export enum UNIVERSAL_ADAPTERS {
|
|
574
|
+
COMMON = 'common_adapter',
|
|
575
|
+
VESU_LEG1 = 'vesu_leg1_adapter',
|
|
576
|
+
VESU_LEG2 = 'vesu_leg2_adapter'
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function getLooperSettings(
|
|
580
|
+
token1Symbol: string,
|
|
581
|
+
token2Symbol: string,
|
|
582
|
+
vaultSettings: UniversalStrategySettings,
|
|
583
|
+
pool1: ContractAddr,
|
|
584
|
+
pool2: ContractAddr
|
|
585
|
+
) {
|
|
586
|
+
const USDCToken = Global.getDefaultTokens().find(token => token.symbol === token1Symbol)!;
|
|
587
|
+
const ETHToken = Global.getDefaultTokens().find(token => token.symbol === token2Symbol)!;
|
|
588
|
+
|
|
589
|
+
const commonAdapter = new CommonAdapter({
|
|
590
|
+
manager: vaultSettings.manager,
|
|
591
|
+
asset: USDCToken.address,
|
|
592
|
+
id: UNIVERSAL_MANAGE_IDS.FLASH_LOAN
|
|
593
|
+
})
|
|
594
|
+
const vesuAdapterUSDCETH = new VesuAdapter({
|
|
595
|
+
poolId: pool1,
|
|
596
|
+
collateral: USDCToken,
|
|
597
|
+
debt: ETHToken,
|
|
598
|
+
vaultAllocator: vaultSettings.vaultAllocator,
|
|
599
|
+
id: UNIVERSAL_MANAGE_IDS.VESU_LEG1
|
|
600
|
+
})
|
|
601
|
+
const vesuAdapterETHUSDC = new VesuAdapter({
|
|
602
|
+
poolId: pool2,
|
|
603
|
+
collateral: ETHToken,
|
|
604
|
+
debt: USDCToken,
|
|
605
|
+
vaultAllocator: vaultSettings.vaultAllocator,
|
|
606
|
+
id: UNIVERSAL_MANAGE_IDS.VESU_LEG2
|
|
607
|
+
})
|
|
608
|
+
vaultSettings.adapters.push(...[{
|
|
609
|
+
id: UNIVERSAL_ADAPTERS.COMMON,
|
|
610
|
+
adapter: commonAdapter
|
|
611
|
+
}, {
|
|
612
|
+
id: UNIVERSAL_ADAPTERS.VESU_LEG1,
|
|
613
|
+
adapter: vesuAdapterUSDCETH
|
|
614
|
+
}, {
|
|
615
|
+
id: UNIVERSAL_ADAPTERS.VESU_LEG2,
|
|
616
|
+
adapter: vesuAdapterETHUSDC
|
|
617
|
+
}])
|
|
618
|
+
vaultSettings.leafAdapters.push(commonAdapter.getFlashloanAdapter.bind(commonAdapter));
|
|
619
|
+
vaultSettings.leafAdapters.push(vesuAdapterUSDCETH.getModifyPosition.bind(vesuAdapterUSDCETH));
|
|
620
|
+
vaultSettings.leafAdapters.push(vesuAdapterETHUSDC.getModifyPosition.bind(vesuAdapterETHUSDC));
|
|
621
|
+
vaultSettings.leafAdapters.push(commonAdapter.getApproveAdapter(USDCToken.address, vesuAdapterUSDCETH.VESU_SINGLETON, UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1).bind(commonAdapter));
|
|
622
|
+
vaultSettings.leafAdapters.push(commonAdapter.getApproveAdapter(ETHToken.address, vesuAdapterETHUSDC.VESU_SINGLETON, UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN2).bind(commonAdapter));
|
|
623
|
+
return vaultSettings;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const _riskFactor: RiskFactor[] = [
|
|
627
|
+
{ type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25, reason: "Audited by Zellic" },
|
|
628
|
+
{ type: RiskType.LIQUIDATION_RISK, value: 1, weight: 50, reason: "Liquidation risk is mitigated btable price feed on Starknet" }
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
const usdcVaultSettings: UniversalStrategySettings = {
|
|
632
|
+
manager: ContractAddr.from('0xf41a2b1f498a7f9629db0b8519259e66e964260a23d20003f3e42bb1997a07'),
|
|
633
|
+
vaultAllocator: ContractAddr.from('0x228cca1005d3f2b55cbaba27cb291dacf1b9a92d1d6b1638195fbd3d0c1e3ba'),
|
|
634
|
+
redeemRequestNFT: ContractAddr.from('0x906d03590010868cbf7590ad47043959d7af8e782089a605d9b22567b64fda'),
|
|
635
|
+
leafAdapters: [],
|
|
636
|
+
adapters: [],
|
|
637
|
+
targetHealthFactor: 1.3,
|
|
638
|
+
minHealthFactor: 1.25
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const wbtcVaultSettings: UniversalStrategySettings = {
|
|
642
|
+
manager: ContractAddr.from('0xef8a664ffcfe46a6af550766d27c28937bf1b77fb4ab54d8553e92bca5ba34'),
|
|
643
|
+
vaultAllocator: ContractAddr.from('0x1e01c25f0d9494570226ad28a7fa856c0640505e809c366a9fab4903320e735'),
|
|
644
|
+
redeemRequestNFT: ContractAddr.from('0x4fec59a12f8424281c1e65a80b5de51b4e754625c60cddfcd00d46941ec37b2'),
|
|
645
|
+
leafAdapters: [],
|
|
646
|
+
adapters: [],
|
|
647
|
+
targetHealthFactor: 1.3,
|
|
648
|
+
minHealthFactor: 1.25
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export const UniversalStrategies: IStrategyMetadata<UniversalStrategySettings>[] =
|
|
652
|
+
[
|
|
653
|
+
{
|
|
654
|
+
name: "USDC Evergreen",
|
|
655
|
+
description: "A universal strategy for managing USDC assets",
|
|
656
|
+
address: ContractAddr.from('0x7e6498cf6a1bfc7e6fc89f1831865e2dacb9756def4ec4b031a9138788a3b5e'),
|
|
657
|
+
launchBlock: 0,
|
|
658
|
+
type: 'ERC4626',
|
|
659
|
+
depositTokens: [Global.getDefaultTokens().find(token => token.symbol === 'USDC')!],
|
|
660
|
+
additionalInfo: getLooperSettings('USDC', 'ETH', usdcVaultSettings, VesuPools.Genesis, VesuPools.Genesis),
|
|
661
|
+
risk: {
|
|
662
|
+
riskFactor: _riskFactor,
|
|
663
|
+
netRisk:
|
|
664
|
+
_riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
|
|
665
|
+
_riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
|
|
666
|
+
notARisks: getNoRiskTags(_riskFactor)
|
|
667
|
+
},
|
|
668
|
+
protocols: [Protocols.VESU],
|
|
669
|
+
maxTVL: Web3Number.fromWei(0, 6),
|
|
670
|
+
contractDetails: [],
|
|
671
|
+
faqs: [],
|
|
672
|
+
investmentSteps: [],
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "WBTC Evergreen",
|
|
676
|
+
description: "A universal strategy for managing WBTC assets",
|
|
677
|
+
address: ContractAddr.from('0x5a4c1651b913aa2ea7afd9024911603152a19058624c3e425405370d62bf80c'),
|
|
678
|
+
launchBlock: 0,
|
|
679
|
+
type: 'ERC4626',
|
|
680
|
+
depositTokens: [Global.getDefaultTokens().find(token => token.symbol === 'WBTC')!],
|
|
681
|
+
additionalInfo: getLooperSettings('WBTC', 'ETH', wbtcVaultSettings, VesuPools.Genesis, VesuPools.Genesis),
|
|
682
|
+
risk: {
|
|
683
|
+
riskFactor: _riskFactor,
|
|
684
|
+
netRisk:
|
|
685
|
+
_riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
|
|
686
|
+
_riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
|
|
687
|
+
notARisks: getNoRiskTags(_riskFactor)
|
|
688
|
+
},
|
|
689
|
+
protocols: [Protocols.VESU],
|
|
690
|
+
maxTVL: Web3Number.fromWei(0, 8),
|
|
691
|
+
contractDetails: [],
|
|
692
|
+
faqs: [],
|
|
693
|
+
investmentSteps: [],
|
|
694
|
+
},
|
|
695
|
+
]
|