@strkfarm/sdk 1.0.15 → 1.0.16
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 +58 -8
- package/dist/cli.mjs +53 -3
- package/dist/index.browser.global.js +1797 -7
- package/dist/index.d.ts +233 -43
- package/dist/index.js +2058 -259
- package/dist/index.mjs +2052 -257
- package/package.json +2 -2
- package/src/data/vesu-rebalance.abi.json +1473 -0
- package/src/dataTypes/bignumber.ts +4 -2
- package/src/global.ts +11 -1
- package/src/index.ts +4 -1
- package/src/interfaces/common.ts +15 -0
- package/src/modules/pricer.ts +12 -5
- package/src/strategies/index.ts +2 -1
- package/src/strategies/vesu-rebalance.ts +429 -0
- package/src/utils/index.ts +7 -4
|
@@ -25,11 +25,13 @@ export class Web3Number extends BigNumber {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
multipliedBy(value: string | number) {
|
|
28
|
-
|
|
28
|
+
let _value = Number(value).toFixed(6);
|
|
29
|
+
return new Web3Number(this.mul(_value).toString(), this.decimals);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
dividedBy(value: string | number) {
|
|
32
|
-
|
|
33
|
+
let _value = Number(value).toFixed(6);
|
|
34
|
+
return new Web3Number(this.div(_value).toString(), this.decimals);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
plus(value: string | number) {
|
package/src/global.ts
CHANGED
|
@@ -45,7 +45,13 @@ export class FatalError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const tokens: TokenInfo[] = [
|
|
48
|
+
const tokens: TokenInfo[] = [{
|
|
49
|
+
name: 'Starknet',
|
|
50
|
+
symbol: 'STRK',
|
|
51
|
+
address: '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
|
|
52
|
+
decimals: 18,
|
|
53
|
+
coingeckId: 'starknet'
|
|
54
|
+
}];
|
|
49
55
|
|
|
50
56
|
/** Contains globally useful functions.
|
|
51
57
|
* - fatalError: Things to do when a fatal error occurs
|
|
@@ -64,6 +70,10 @@ export class Global {
|
|
|
64
70
|
console.error(err);
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
static getDefaultTokens() {
|
|
74
|
+
return tokens;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
static async getTokens(): Promise<TokenInfo[]> {
|
|
68
78
|
if (tokens.length) return tokens;
|
|
69
79
|
|
package/src/index.ts
CHANGED
package/src/interfaces/common.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ContractAddr } from "@/dataTypes"
|
|
1
2
|
import { BlockIdentifier, RpcProvider } from "starknet"
|
|
2
3
|
|
|
3
4
|
export interface TokenInfo {
|
|
@@ -21,6 +22,20 @@ export interface IConfig {
|
|
|
21
22
|
heartbeatUrl?: string
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export interface IProtocol {
|
|
26
|
+
name: string,
|
|
27
|
+
logo: string,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IStrategyMetadata {
|
|
31
|
+
name: string,
|
|
32
|
+
description: string,
|
|
33
|
+
address: ContractAddr,
|
|
34
|
+
type: 'ERC4626' | 'ERC721' | 'Other',
|
|
35
|
+
depositTokens: TokenInfo[],
|
|
36
|
+
protocols: IProtocol[]
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export function getMainnetConfig(rpcUrl = "https://starknet-mainnet.public.blastapi.io", blockIdentifier: BlockIdentifier = 'pending'): IConfig {
|
|
25
40
|
return {
|
|
26
41
|
provider: new RpcProvider({
|
package/src/modules/pricer.ts
CHANGED
|
@@ -9,7 +9,14 @@ export interface PriceInfo {
|
|
|
9
9
|
price: number,
|
|
10
10
|
timestamp: Date
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
export abstract class PricerBase {
|
|
14
|
+
async getPrice(tokenSymbol: string): Promise<PriceInfo> {
|
|
15
|
+
throw new Error('Method not implemented');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Pricer implements PricerBase{
|
|
13
20
|
readonly config: IConfig;
|
|
14
21
|
readonly tokens: TokenInfo[] = [];
|
|
15
22
|
protected prices: {
|
|
@@ -80,10 +87,10 @@ export class Pricer {
|
|
|
80
87
|
Global.assert(!this.isStale(timestamp, tokenName), `Price of ${tokenName} is stale`);
|
|
81
88
|
|
|
82
89
|
}
|
|
83
|
-
async getPrice(
|
|
84
|
-
Global.assert(this.prices[
|
|
85
|
-
this.assertNotStale(this.prices[
|
|
86
|
-
return this.prices[
|
|
90
|
+
async getPrice(tokenSymbol: string) {
|
|
91
|
+
Global.assert(this.prices[tokenSymbol], `Price of ${tokenSymbol} not found`);
|
|
92
|
+
this.assertNotStale(this.prices[tokenSymbol].timestamp, tokenSymbol);
|
|
93
|
+
return this.prices[tokenSymbol];
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
protected _loadPrices(onUpdate: (tokenSymbol: string) => void = () => {}) {
|
package/src/strategies/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './autoCompounderStrk';
|
|
1
|
+
export * from './autoCompounderStrk';
|
|
2
|
+
export * from './vesu-rebalance';
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { IConfig, IProtocol, IStrategyMetadata } from "@/interfaces";
|
|
3
|
+
import { Pricer, PricerBase } from "@/modules";
|
|
4
|
+
import { CairoCustomEnum, Contract, num, uint256 } from "starknet";
|
|
5
|
+
import VesuRebalanceAbi from '@/data/vesu-rebalance.abi.json';
|
|
6
|
+
import { Global } from "@/global";
|
|
7
|
+
import { assert } from "@/utils";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
|
|
10
|
+
interface PoolProps {
|
|
11
|
+
pool_id: ContractAddr;
|
|
12
|
+
max_weight: number;
|
|
13
|
+
v_token: ContractAddr;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Change {
|
|
17
|
+
pool_id: ContractAddr;
|
|
18
|
+
changeAmt: Web3Number;
|
|
19
|
+
finalAmt: Web3Number;
|
|
20
|
+
isDeposit: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PoolInfoFull {
|
|
24
|
+
pool_id: ContractAddr;
|
|
25
|
+
pool_name: string | undefined;
|
|
26
|
+
max_weight: number;
|
|
27
|
+
current_weight: number;
|
|
28
|
+
v_token: ContractAddr;
|
|
29
|
+
amount: Web3Number;
|
|
30
|
+
usdValue: Web3Number;
|
|
31
|
+
APY: {
|
|
32
|
+
baseApy: number;
|
|
33
|
+
defiSpringApy: number;
|
|
34
|
+
netApy: number;
|
|
35
|
+
};
|
|
36
|
+
currentUtilization: number;
|
|
37
|
+
maxUtilization: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Represents a VesuRebalance strategy.
|
|
41
|
+
* This class implements an automated rebalancing strategy for Vesu pools,
|
|
42
|
+
* managing deposits and withdrawals while optimizing yield through STRK rewards.
|
|
43
|
+
*/
|
|
44
|
+
export class VesuRebalance {
|
|
45
|
+
/** Configuration object for the strategy */
|
|
46
|
+
readonly config: IConfig;
|
|
47
|
+
/** Contract address of the strategy */
|
|
48
|
+
readonly address: ContractAddr;
|
|
49
|
+
/** Pricer instance for token price calculations */
|
|
50
|
+
readonly pricer: PricerBase;
|
|
51
|
+
/** Metadata containing strategy information */
|
|
52
|
+
readonly metadata: IStrategyMetadata;
|
|
53
|
+
/** Contract instance for interacting with the strategy */
|
|
54
|
+
readonly contract: Contract;
|
|
55
|
+
readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new VesuRebalance strategy instance.
|
|
59
|
+
* @param config - Configuration object containing provider and other settings
|
|
60
|
+
* @param pricer - Pricer instance for token price calculations
|
|
61
|
+
* @param metadata - Strategy metadata including deposit tokens and address
|
|
62
|
+
* @throws {Error} If more than one deposit token is specified
|
|
63
|
+
*/
|
|
64
|
+
constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata) {
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.pricer = pricer;
|
|
67
|
+
|
|
68
|
+
assert(metadata.depositTokens.length === 1, 'VesuRebalance only supports 1 deposit token');
|
|
69
|
+
this.metadata = metadata;
|
|
70
|
+
this.address = metadata.address;
|
|
71
|
+
|
|
72
|
+
this.contract = new Contract(VesuRebalanceAbi, this.address.address, this.config.provider);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a deposit call to the strategy contract.
|
|
77
|
+
* @param assets - Amount of assets to deposit
|
|
78
|
+
* @param receiver - Address that will receive the strategy tokens
|
|
79
|
+
* @returns Populated contract call for deposit
|
|
80
|
+
*/
|
|
81
|
+
depositCall(assets: Web3Number, receiver: ContractAddr) {
|
|
82
|
+
// Technically its not erc4626 abi, but we just need approve call
|
|
83
|
+
// so, its ok to use it
|
|
84
|
+
const assetContract = new Contract(VesuRebalanceAbi, this.metadata.depositTokens[0].address, this.config.provider);
|
|
85
|
+
const call1 = assetContract.populate('approve', [this.address.address, uint256.bnToUint256(assets.toWei())]);
|
|
86
|
+
const call2 = this.contract.populate('deposit', [uint256.bnToUint256(assets.toWei()), receiver.address]);
|
|
87
|
+
return [call1, call2];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a withdrawal call to the strategy contract.
|
|
92
|
+
* @param assets - Amount of assets to withdraw
|
|
93
|
+
* @param receiver - Address that will receive the withdrawn assets
|
|
94
|
+
* @param owner - Address that owns the strategy tokens
|
|
95
|
+
* @returns Populated contract call for withdrawal
|
|
96
|
+
*/
|
|
97
|
+
withdrawCall(assets: Web3Number, receiver: ContractAddr, owner: ContractAddr) {
|
|
98
|
+
return [this.contract.populate('withdraw', [uint256.bnToUint256(assets.toWei()), receiver.address, owner.address])];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns the underlying asset token of the strategy.
|
|
103
|
+
* @returns The deposit token supported by this strategy
|
|
104
|
+
*/
|
|
105
|
+
asset() {
|
|
106
|
+
return this.metadata.depositTokens[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns the number of decimals used by the strategy token.
|
|
111
|
+
* @returns Number of decimals (same as the underlying token)
|
|
112
|
+
*/
|
|
113
|
+
decimals() {
|
|
114
|
+
return this.metadata.depositTokens[0].decimals; // same as underlying token
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calculates the Total Value Locked (TVL) for a specific user.
|
|
119
|
+
* @param user - Address of the user
|
|
120
|
+
* @returns Object containing the amount in token units and USD value
|
|
121
|
+
*/
|
|
122
|
+
async getUserTVL(user: ContractAddr) {
|
|
123
|
+
const shares = await this.contract.balanceOf(user.address);
|
|
124
|
+
const assets = await this.contract.convert_to_assets(uint256.bnToUint256(shares));
|
|
125
|
+
const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
|
|
126
|
+
let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
|
|
127
|
+
const usdValue = Number(amount.toFixed(6)) * price.price;
|
|
128
|
+
return {
|
|
129
|
+
amount,
|
|
130
|
+
usdValue
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Calculates the total TVL of the strategy.
|
|
136
|
+
* @returns Object containing the total amount in token units and USD value
|
|
137
|
+
*/
|
|
138
|
+
async getTVL() {
|
|
139
|
+
const assets = await this.contract.total_assets();
|
|
140
|
+
const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
|
|
141
|
+
let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
|
|
142
|
+
const usdValue = Number(amount.toFixed(6)) * price.price;
|
|
143
|
+
return {
|
|
144
|
+
amount,
|
|
145
|
+
usdValue
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Retrieves the list of allowed pools and their detailed information from multiple sources:
|
|
151
|
+
* 1. Contract's allowed pools
|
|
152
|
+
* 2. Vesu positions API for current positions
|
|
153
|
+
* 3. Vesu pools API for APY and utilization data
|
|
154
|
+
*
|
|
155
|
+
* @returns {Promise<{
|
|
156
|
+
* data: Array<PoolInfoFull>,
|
|
157
|
+
* isErrorPositionsAPI: boolean
|
|
158
|
+
* }>} Object containing:
|
|
159
|
+
* - data: Array of pool information including IDs, weights, amounts, APYs and utilization
|
|
160
|
+
* - isErrorPositionsAPI: Boolean indicating if there was an error fetching position data
|
|
161
|
+
*/
|
|
162
|
+
async getPools() {
|
|
163
|
+
const allowedPools: PoolProps[] = (await this.contract.get_allowed_pools()).map((p: any) => ({
|
|
164
|
+
pool_id: ContractAddr.from(p.pool_id),
|
|
165
|
+
max_weight: Number(p.max_weight) / this.BASE_WEIGHT,
|
|
166
|
+
v_token: ContractAddr.from(p.v_token),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
Vesu Positions API Response Schema (/positions?walletAddress=):
|
|
171
|
+
{
|
|
172
|
+
"data": [{
|
|
173
|
+
"pool": {
|
|
174
|
+
"id": string, // Pool identifier
|
|
175
|
+
"name": string // Pool name
|
|
176
|
+
},
|
|
177
|
+
"collateral": {
|
|
178
|
+
"value": string, // Amount of collateral in wei
|
|
179
|
+
"usdPrice": {
|
|
180
|
+
"value": string, // USD value in wei
|
|
181
|
+
"decimals": number // Decimals for USD value
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Vesu Pools API Response Schema (/pools):
|
|
188
|
+
{
|
|
189
|
+
"data": [{
|
|
190
|
+
"id": string,
|
|
191
|
+
"assets": [{
|
|
192
|
+
"stats": {
|
|
193
|
+
"supplyApy": {
|
|
194
|
+
"value": string,
|
|
195
|
+
"decimals": number
|
|
196
|
+
},
|
|
197
|
+
"defiSpringSupplyApr": {
|
|
198
|
+
"value": string,
|
|
199
|
+
"decimals": number
|
|
200
|
+
},
|
|
201
|
+
"currentUtilization": {
|
|
202
|
+
"value": string,
|
|
203
|
+
"decimals": number
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"config": {
|
|
207
|
+
"maxUtilization": {
|
|
208
|
+
"value": string,
|
|
209
|
+
"decimals": number
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}],
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
*/
|
|
216
|
+
let isErrorPositionsAPI = false;
|
|
217
|
+
let vesuPositions: any[] = [];
|
|
218
|
+
try {
|
|
219
|
+
const res = await axios.get(`https://api.vesu.xyz/positions?walletAddress=${this.address.address}`)
|
|
220
|
+
const data = await res.data;
|
|
221
|
+
vesuPositions = data.data;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
|
|
224
|
+
isErrorPositionsAPI = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
let isErrorPoolsAPI = false;
|
|
229
|
+
let pools: any[] = [];
|
|
230
|
+
try {
|
|
231
|
+
const res = await axios.get(`https://api.vesu.xyz/pools`);
|
|
232
|
+
const data = await res.data;
|
|
233
|
+
pools = data.data;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
|
|
236
|
+
isErrorPoolsAPI = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const totalAssets = (await this.getTVL()).amount;
|
|
240
|
+
|
|
241
|
+
const info = allowedPools.map(async (p) => {
|
|
242
|
+
const vesuPosition = vesuPositions.find((d: any) => d.pool.id.toString() === num.getDecimalString(p.pool_id.address.toString()));
|
|
243
|
+
const pool = pools.find((d: any) => d.id == num.getDecimalString(p.pool_id.address));
|
|
244
|
+
const assetInfo = pool?.assets.find((d: any) => ContractAddr.from(this.asset().address).eqString(d.address));
|
|
245
|
+
let vTokenContract = new Contract(VesuRebalanceAbi, p.v_token.address, this.config.provider);
|
|
246
|
+
const bal = await vTokenContract.balanceOf(this.address.address);
|
|
247
|
+
const assets = await vTokenContract.convert_to_assets(uint256.bnToUint256(bal.toString()));
|
|
248
|
+
const item = {
|
|
249
|
+
pool_id: p.pool_id,
|
|
250
|
+
pool_name: vesuPosition?.pool.name,
|
|
251
|
+
max_weight: p.max_weight,
|
|
252
|
+
current_weight: isErrorPositionsAPI || !vesuPosition ? 0 : Number(Web3Number.fromWei(vesuPosition.collateral.value, this.decimals()).dividedBy(totalAssets.toString()).toFixed(6)),
|
|
253
|
+
v_token: p.v_token,
|
|
254
|
+
amount: Web3Number.fromWei(assets.toString(), this.decimals()),
|
|
255
|
+
usdValue: isErrorPositionsAPI || !vesuPosition ? Web3Number.fromWei("0", this.decimals()) : Web3Number.fromWei(vesuPosition.collateral.usdPrice.value, vesuPosition.collateral.usdPrice.decimals),
|
|
256
|
+
APY: isErrorPoolsAPI || !assetInfo ? {
|
|
257
|
+
baseApy: 0,
|
|
258
|
+
defiSpringApy: 0,
|
|
259
|
+
netApy: 0,
|
|
260
|
+
} : {
|
|
261
|
+
baseApy: Number(Web3Number.fromWei(assetInfo.stats.supplyApy.value, assetInfo.stats.supplyApy.decimals).toFixed(6)),
|
|
262
|
+
defiSpringApy: Number(Web3Number.fromWei(assetInfo.stats.defiSpringSupplyApr.value, assetInfo.stats.defiSpringSupplyApr.decimals).toFixed(6)),
|
|
263
|
+
netApy: 0,
|
|
264
|
+
},
|
|
265
|
+
currentUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.stats.currentUtilization.value, assetInfo.stats.currentUtilization.decimals).toFixed(6)),
|
|
266
|
+
maxUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.config.maxUtilization.value, assetInfo.config.maxUtilization.decimals).toFixed(6)),
|
|
267
|
+
}
|
|
268
|
+
item.APY.netApy = item.APY.baseApy + item.APY.defiSpringApy;
|
|
269
|
+
return item;
|
|
270
|
+
});
|
|
271
|
+
const data = await Promise.all(info);
|
|
272
|
+
return {
|
|
273
|
+
data,
|
|
274
|
+
isErrorPositionsAPI,
|
|
275
|
+
isErrorPoolsAPI,
|
|
276
|
+
isError: isErrorPositionsAPI || isErrorPoolsAPI,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Calculates the weighted average APY across all pools based on USD value.
|
|
282
|
+
* @returns {Promise<number>} The weighted average APY across all pools
|
|
283
|
+
*/
|
|
284
|
+
async netAPY(): Promise<number> {
|
|
285
|
+
const { data: pools } = await this.getPools();
|
|
286
|
+
return this.netAPYGivenPools(pools);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Calculates the weighted average APY across all pools based on USD value.
|
|
291
|
+
* @returns {Promise<number>} The weighted average APY across all pools
|
|
292
|
+
*/
|
|
293
|
+
netAPYGivenPools(pools: PoolInfoFull[]): number {
|
|
294
|
+
const weightedApy = pools.reduce((acc: number, curr) => {
|
|
295
|
+
const weight = curr.current_weight;
|
|
296
|
+
return acc + (curr.APY.netApy * weight);
|
|
297
|
+
}, 0);
|
|
298
|
+
|
|
299
|
+
return weightedApy;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Calculates optimal position changes to maximize APY while respecting max weights.
|
|
304
|
+
* The algorithm:
|
|
305
|
+
* 1. Sorts pools by APY (highest first)
|
|
306
|
+
* 2. Calculates target amounts based on max weights
|
|
307
|
+
* 3. For each pool that needs more funds:
|
|
308
|
+
* - Takes funds from lowest APY pools that are over their target
|
|
309
|
+
* 4. Validates that total assets remain constant
|
|
310
|
+
*
|
|
311
|
+
* @returns {Promise<{
|
|
312
|
+
* changes: Change[],
|
|
313
|
+
* finalPools: PoolInfoFull[],
|
|
314
|
+
* isAnyPoolOverMaxWeight: boolean
|
|
315
|
+
* }>} Object containing:
|
|
316
|
+
* - changes: Array of position changes
|
|
317
|
+
* - finalPools: Array of pool information after rebalance
|
|
318
|
+
* @throws Error if rebalance is not possible while maintaining constraints
|
|
319
|
+
*/
|
|
320
|
+
async getRebalancedPositions() {
|
|
321
|
+
const { data: pools } = await this.getPools();
|
|
322
|
+
const totalAssets = (await this.getTVL()).amount;
|
|
323
|
+
if (totalAssets.eq(0)) return {
|
|
324
|
+
changes: [],
|
|
325
|
+
finalPools: [],
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// assert sum of pools.amount <= totalAssets
|
|
329
|
+
const sumPools = pools.reduce((acc, curr) => acc.plus(curr.amount.toString()), Web3Number.fromWei("0", this.decimals()));
|
|
330
|
+
assert(sumPools.lte(totalAssets), 'Sum of pools.amount must be less than or equal to totalAssets');
|
|
331
|
+
|
|
332
|
+
// Sort pools by APY and calculate target amounts
|
|
333
|
+
const sortedPools = [...pools].sort((a, b) => b.APY.netApy - a.APY.netApy);
|
|
334
|
+
const targetAmounts: Record<string, Web3Number> = {};
|
|
335
|
+
let remainingAssets = totalAssets;
|
|
336
|
+
|
|
337
|
+
// First pass: Allocate to high APY pools up to their max weight
|
|
338
|
+
let isAnyPoolOverMaxWeight = false;
|
|
339
|
+
for (const pool of sortedPools) {
|
|
340
|
+
const maxAmount = totalAssets.multipliedBy(pool.max_weight * 0.9); // 10% tolerance
|
|
341
|
+
const targetAmount = remainingAssets.gte(maxAmount) ? maxAmount : remainingAssets;
|
|
342
|
+
targetAmounts[pool.pool_id.address.toString()] = targetAmount;
|
|
343
|
+
remainingAssets = remainingAssets.minus(targetAmount.toString());
|
|
344
|
+
if (pool.current_weight > pool.max_weight) {
|
|
345
|
+
isAnyPoolOverMaxWeight = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
assert(remainingAssets.lt(0.00001), 'Remaining assets must be 0');
|
|
350
|
+
|
|
351
|
+
// Calculate required changes
|
|
352
|
+
const changes: Change[] = sortedPools.map(pool => {
|
|
353
|
+
const target = targetAmounts[pool.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
|
|
354
|
+
const change = Web3Number.fromWei(target.minus(pool.amount.toString()).toWei(), this.decimals());
|
|
355
|
+
return {
|
|
356
|
+
pool_id: pool.pool_id,
|
|
357
|
+
changeAmt: change,
|
|
358
|
+
finalAmt: target,
|
|
359
|
+
isDeposit: change.gt(0)
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Validate changes
|
|
364
|
+
const sumChanges = changes.reduce((sum, c) => sum.plus(c.changeAmt.toString()), Web3Number.fromWei("0", this.decimals()));
|
|
365
|
+
const sumFinal = changes.reduce((sum, c) => sum.plus(c.finalAmt.toString()), Web3Number.fromWei("0", this.decimals()));
|
|
366
|
+
const hasChanges = changes.some(c => !c.changeAmt.eq(0));
|
|
367
|
+
|
|
368
|
+
if (!sumChanges.eq(0)) throw new Error('Sum of changes must be zero');
|
|
369
|
+
if (!sumFinal.eq(totalAssets)) throw new Error('Sum of final amounts must equal total assets');
|
|
370
|
+
if (!hasChanges) throw new Error('No changes required');
|
|
371
|
+
|
|
372
|
+
const finalPools: PoolInfoFull[] = pools.map((p) => {
|
|
373
|
+
const target = targetAmounts[p.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
|
|
374
|
+
return {
|
|
375
|
+
...p,
|
|
376
|
+
amount: target,
|
|
377
|
+
usdValue: Web3Number.fromWei("0", this.decimals()),
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
changes,
|
|
382
|
+
finalPools,
|
|
383
|
+
isAnyPoolOverMaxWeight,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Creates a rebalance Call object for the strategy contract
|
|
389
|
+
* @param pools - Array of pool information including IDs, weights, amounts, APYs and utilization
|
|
390
|
+
* @returns Populated contract call for rebalance
|
|
391
|
+
*/
|
|
392
|
+
async getRebalanceCall(
|
|
393
|
+
pools: Awaited<ReturnType<typeof this.getRebalancedPositions>>['changes'],
|
|
394
|
+
isOverWeightAdjustment: boolean // here, yield increase doesnt matter
|
|
395
|
+
) {
|
|
396
|
+
const actions: any[] = [];
|
|
397
|
+
// sort to put withdrawals first
|
|
398
|
+
pools.sort((a, b) => b.isDeposit ? -1 : 1);
|
|
399
|
+
console.log('pools', pools);
|
|
400
|
+
pools.forEach((p) => {
|
|
401
|
+
if (p.changeAmt.eq(0)) return null;
|
|
402
|
+
actions.push({
|
|
403
|
+
pool_id: p.pool_id.address,
|
|
404
|
+
feature: new CairoCustomEnum(p.isDeposit ? {DEPOSIT: {}} : {WITHDRAW: {}}),
|
|
405
|
+
token: this.asset().address,
|
|
406
|
+
amount: uint256.bnToUint256(p.changeAmt.multipliedBy(p.isDeposit ? 1 : -1).toWei()),
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
if (actions.length === 0) return null;
|
|
410
|
+
if (isOverWeightAdjustment) {
|
|
411
|
+
return this.contract.populate('rebalance_weights', [actions]);
|
|
412
|
+
}
|
|
413
|
+
return this.contract.populate('rebalance', [actions]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const _description = 'Automatically diversify {{TOKEN}} holdings into different Vesu pools while reducing risk and maximizing yield. Defi spring STRK Rewards are auto-compounded as well.'
|
|
418
|
+
const _protocol: IProtocol = {name: 'Vesu', logo: 'https://static-assets-8zct.onrender.com/integrations/vesu/logo.png'}
|
|
419
|
+
/**
|
|
420
|
+
* Represents the Vesu Rebalance Strategies.
|
|
421
|
+
*/
|
|
422
|
+
export const VesuRebalanceStrategies: IStrategyMetadata[] = [{
|
|
423
|
+
name: 'Vesu STRK',
|
|
424
|
+
description: _description.replace('{{TOKEN}}', 'STRK'),
|
|
425
|
+
address: ContractAddr.from('0xeeb729d554ae486387147b13a9c8871bc7991d454e8b5ff570d4bf94de71e1'),
|
|
426
|
+
type: 'ERC4626',
|
|
427
|
+
depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'STRK')!],
|
|
428
|
+
protocols: [_protocol]
|
|
429
|
+
}]
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export * from './store';
|
|
2
|
-
export * from './encrypt';
|
|
3
|
-
|
|
4
1
|
// Utility type to make all optional properties required
|
|
5
2
|
export type RequiredFields<T> = {
|
|
6
3
|
[K in keyof T]-?: T[K]
|
|
@@ -9,4 +6,10 @@ export type RequiredFields<T> = {
|
|
|
9
6
|
// Utility type to get only the required fields of a type
|
|
10
7
|
export type RequiredKeys<T> = {
|
|
11
8
|
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
|
|
12
|
-
}[keyof T]
|
|
9
|
+
}[keyof T]
|
|
10
|
+
|
|
11
|
+
export function assert(condition: boolean, message: string) {
|
|
12
|
+
if (!condition) {
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
}
|
|
15
|
+
}
|