@strkfarm/sdk 1.1.34 → 1.1.36
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 +2732 -45
- package/dist/index.browser.mjs +2732 -45
- package/dist/index.d.ts +57 -4
- package/dist/index.js +2733 -45
- package/dist/index.mjs +2732 -45
- package/package.json +1 -1
- package/src/data/vesu-extension.abi.json +2443 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +201 -0
- package/src/strategies/universal-lst-muliplier-strategy.tsx +162 -40
- package/src/strategies/universal-strategy.tsx +33 -18
|
@@ -14,6 +14,7 @@ import { ENDPOINTS } from "../constants";
|
|
|
14
14
|
import VesuMultiplyAbi from '@/data/vesu-multiple.abi.json';
|
|
15
15
|
import { EkuboPoolKey } from "../ekubo-cl-vault";
|
|
16
16
|
import VesuPoolV2Abi from '@/data/vesu-pool-v2.abi.json';
|
|
17
|
+
import VesuExtensionAbi from '@/data/vesu-extension.abi.json';
|
|
17
18
|
|
|
18
19
|
interface VesuPoolsInfo { pools: any[]; isErrorPoolsAPI: boolean };
|
|
19
20
|
|
|
@@ -56,6 +57,23 @@ export interface VesuAdapterConfig {
|
|
|
56
57
|
id: string
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
type InterestRateConfig = {
|
|
61
|
+
target_utilization: bigint;
|
|
62
|
+
zero_utilization_rate: bigint;
|
|
63
|
+
target_rate_percent: bigint;
|
|
64
|
+
|
|
65
|
+
min_target_utilization: bigint;
|
|
66
|
+
max_target_utilization: bigint;
|
|
67
|
+
rate_half_life: bigint;
|
|
68
|
+
min_full_utilization_rate: bigint;
|
|
69
|
+
max_full_utilization_rate: bigint;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SCALE = BigInt(1e18);
|
|
73
|
+
const UTILIZATION_SCALE = 100_000n;
|
|
74
|
+
const UTILIZATION_SCALE_TO_SCALE = BigInt(1e13);
|
|
75
|
+
|
|
76
|
+
|
|
59
77
|
export interface TokenAmount {
|
|
60
78
|
token: ContractAddr,
|
|
61
79
|
amount: Web3Number, // i129
|
|
@@ -217,6 +235,9 @@ export const VesuPools = {
|
|
|
217
235
|
Re7xBTC: ContractAddr.from('0x3a8416bf20d036df5b1cf3447630a2e1cb04685f6b0c3a70ed7fb1473548ecf')
|
|
218
236
|
}
|
|
219
237
|
|
|
238
|
+
export const extensionMap: {[key: string]: ContractAddr} = {};
|
|
239
|
+
extensionMap[VesuPools.Re7xSTRK.address] = ContractAddr.from('0x04e06e04b8d624d039aa1c3ca8e0aa9e21dc1ccba1d88d0d650837159e0ee054');
|
|
240
|
+
|
|
220
241
|
export function getVesuSingletonAddress(vesuPool: ContractAddr) {
|
|
221
242
|
if (vesuPool.eq(VesuPools.Genesis) ||
|
|
222
243
|
vesuPool.eq(VesuPools.Re7xSTRK)) {
|
|
@@ -501,6 +522,67 @@ export class VesuAdapter extends BaseAdapter {
|
|
|
501
522
|
}
|
|
502
523
|
}
|
|
503
524
|
|
|
525
|
+
async getDebtCap(config: IConfig) {
|
|
526
|
+
const { contract, isV2 } = await this.getVesuSingletonContract(config, this.config.poolId);
|
|
527
|
+
if (!isV2) {
|
|
528
|
+
const extensionAddr = extensionMap[this.config.poolId.address];
|
|
529
|
+
if (!extensionAddr) {
|
|
530
|
+
throw new Error('Extension address not found');
|
|
531
|
+
}
|
|
532
|
+
const extensionContract = new Contract({abi: VesuExtensionAbi, address: extensionAddr.address, providerOrAccount: config.provider});
|
|
533
|
+
const output: any = await extensionContract.call('debt_caps', [this.config.poolId.address, this.config.collateral.address.address, this.config.debt.address.address]);
|
|
534
|
+
logger.verbose(`${this.config.debt.symbol}::VesuAdapter::getDebtCap debt_cap: ${output.toString()}`);
|
|
535
|
+
return Web3Number.fromWei(output.toString(), this.config.debt.decimals);
|
|
536
|
+
}
|
|
537
|
+
const output: any = await contract.call('pair_config', [this.config.collateral.address.address, this.config.debt.address.address]);
|
|
538
|
+
logger.verbose(`${this.config.debt.symbol}::VesuAdapter::getDebtCap debt_cap: ${output.debt_cap.toString()}`);
|
|
539
|
+
return Web3Number.fromWei(output.debt_cap.toString(), this.config.debt.decimals);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async getMaxBorrowableByInterestRate(config: IConfig, asset: TokenInfo, maxBorrowAPY: number) {
|
|
543
|
+
const { contract, isV2 } = await this.getVesuSingletonContract(config, this.config.poolId);
|
|
544
|
+
let interestRateConfigContract = contract;
|
|
545
|
+
if (!isV2) {
|
|
546
|
+
const extensionAddr = extensionMap[this.config.poolId.address];
|
|
547
|
+
if (!extensionAddr) {
|
|
548
|
+
throw new Error('Extension address not found');
|
|
549
|
+
}
|
|
550
|
+
interestRateConfigContract = new Contract({abi: VesuExtensionAbi, address: extensionAddr.address, providerOrAccount: config.provider});
|
|
551
|
+
|
|
552
|
+
}
|
|
553
|
+
const _interestRateConfig: any = await interestRateConfigContract.call(
|
|
554
|
+
'interest_rate_config',
|
|
555
|
+
isV2 ? [this.config.debt.address.address] : [this.config.poolId.address, this.config.debt.address.address]
|
|
556
|
+
);
|
|
557
|
+
const interestRateConfig: InterestRateConfig = {
|
|
558
|
+
target_utilization: _interestRateConfig.target_utilization,
|
|
559
|
+
zero_utilization_rate: _interestRateConfig.zero_utilization_rate,
|
|
560
|
+
target_rate_percent: _interestRateConfig.target_rate_percent,
|
|
561
|
+
min_target_utilization: _interestRateConfig.min_target_utilization,
|
|
562
|
+
max_target_utilization: _interestRateConfig.max_target_utilization,
|
|
563
|
+
rate_half_life: _interestRateConfig.rate_half_life,
|
|
564
|
+
min_full_utilization_rate: _interestRateConfig.min_full_utilization_rate,
|
|
565
|
+
max_full_utilization_rate: _interestRateConfig.max_full_utilization_rate,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const _assetConfig: any = await contract.call(
|
|
569
|
+
isV2 ? 'asset_config' : 'asset_config_unsafe',
|
|
570
|
+
isV2 ? [asset.address.address] : [this.config.poolId.address, asset.address.address]
|
|
571
|
+
);
|
|
572
|
+
const assetConfig = isV2 ? _assetConfig : _assetConfig['0'];
|
|
573
|
+
const timeDelta = assetConfig.last_updated;
|
|
574
|
+
const lastFullUtilizationRate = assetConfig.last_full_utilization_rate;
|
|
575
|
+
const totalSupply = (new Web3Number((Number(assetConfig.total_nominal_debt) / 1e18).toFixed(9), asset.decimals)).plus(Web3Number.fromWei(assetConfig.reserve, asset.decimals));
|
|
576
|
+
|
|
577
|
+
const ratePerSecond = BigInt(Math.round(maxBorrowAPY / 365 / 24 / 60 / 60 * Number(SCALE)));
|
|
578
|
+
const maxUtilisation = this.getMaxUtilizationGivenRatePerSecond(interestRateConfig, ratePerSecond, timeDelta, lastFullUtilizationRate);
|
|
579
|
+
logger.verbose(`${asset.symbol}::VesuAdapter::getMaxBorrowableByInterestRate maxUtilisation: ${Number(maxUtilisation) / 1e18}, totalSupply: ${totalSupply.toString()}`);
|
|
580
|
+
|
|
581
|
+
const maxDebtToHave = totalSupply.multipliedBy(Number(maxUtilisation) / 1e18);
|
|
582
|
+
const currentDebt = new Web3Number((Number(assetConfig.total_nominal_debt) / 1e18).toFixed(9), asset.decimals);
|
|
583
|
+
return maxDebtToHave.minus(currentDebt);
|
|
584
|
+
}
|
|
585
|
+
|
|
504
586
|
async getLTVConfig(config: IConfig) {
|
|
505
587
|
const CACHE_KEY = 'ltv_config';
|
|
506
588
|
const cacheData = this.getCache<number>(CACHE_KEY);
|
|
@@ -516,6 +598,9 @@ export class VesuAdapter extends BaseAdapter {
|
|
|
516
598
|
const output: any = await contract.call('ltv_config', [this.config.poolId.address, this.config.collateral.address.address, this.config.debt.address.address]);
|
|
517
599
|
ltv = Number(output.max_ltv) / 1e18;
|
|
518
600
|
}
|
|
601
|
+
if (ltv == 0) {
|
|
602
|
+
throw new Error('LTV is 0');
|
|
603
|
+
}
|
|
519
604
|
this.setCache(CACHE_KEY, ltv, 300000); // ttl: 5min
|
|
520
605
|
return this.getCache<number>(CACHE_KEY) as number;
|
|
521
606
|
}
|
|
@@ -673,4 +758,120 @@ export class VesuAdapter extends BaseAdapter {
|
|
|
673
758
|
Global.setGlobalCache(CACHE_KEY, { pools, isErrorPoolsAPI }, 300000); // cache for 5 minutes
|
|
674
759
|
return { pools, isErrorPoolsAPI };
|
|
675
760
|
}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
fullUtilizationRate(
|
|
764
|
+
interestRateConfig: InterestRateConfig,
|
|
765
|
+
timeDelta: bigint,
|
|
766
|
+
utilization: bigint,
|
|
767
|
+
fullUtilizationRate: bigint
|
|
768
|
+
): bigint {
|
|
769
|
+
const {
|
|
770
|
+
min_target_utilization,
|
|
771
|
+
max_target_utilization,
|
|
772
|
+
rate_half_life,
|
|
773
|
+
min_full_utilization_rate,
|
|
774
|
+
max_full_utilization_rate,
|
|
775
|
+
} = interestRateConfig;
|
|
776
|
+
|
|
777
|
+
const halfLifeScaled = rate_half_life * SCALE;
|
|
778
|
+
|
|
779
|
+
let nextFullUtilizationRate: bigint;
|
|
780
|
+
|
|
781
|
+
if (utilization < min_target_utilization) {
|
|
782
|
+
const utilizationDelta =
|
|
783
|
+
((min_target_utilization - utilization) * SCALE) / min_target_utilization;
|
|
784
|
+
const decay = halfLifeScaled + utilizationDelta * timeDelta;
|
|
785
|
+
nextFullUtilizationRate = (fullUtilizationRate * halfLifeScaled) / decay;
|
|
786
|
+
} else if (utilization > max_target_utilization) {
|
|
787
|
+
const utilizationDelta =
|
|
788
|
+
((utilization - max_target_utilization) * SCALE) /
|
|
789
|
+
(UTILIZATION_SCALE - max_target_utilization);
|
|
790
|
+
const growth = halfLifeScaled + utilizationDelta * timeDelta;
|
|
791
|
+
nextFullUtilizationRate = (fullUtilizationRate * growth) / halfLifeScaled;
|
|
792
|
+
} else {
|
|
793
|
+
nextFullUtilizationRate = fullUtilizationRate;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (nextFullUtilizationRate > max_full_utilization_rate) {
|
|
797
|
+
return max_full_utilization_rate;
|
|
798
|
+
} else if (nextFullUtilizationRate < min_full_utilization_rate) {
|
|
799
|
+
return min_full_utilization_rate;
|
|
800
|
+
} else {
|
|
801
|
+
return nextFullUtilizationRate;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Calculates new interest rate per second and next full utilization rate.
|
|
807
|
+
*/
|
|
808
|
+
calculateInterestRate(
|
|
809
|
+
interestRateConfig: InterestRateConfig,
|
|
810
|
+
utilization: bigint,
|
|
811
|
+
timeDelta: bigint,
|
|
812
|
+
lastFullUtilizationRate: bigint
|
|
813
|
+
): { newRatePerSecond: bigint; nextFullUtilizationRate: bigint } {
|
|
814
|
+
const scaledUtilization = utilization / UTILIZATION_SCALE_TO_SCALE;
|
|
815
|
+
|
|
816
|
+
const {
|
|
817
|
+
target_utilization,
|
|
818
|
+
zero_utilization_rate,
|
|
819
|
+
target_rate_percent,
|
|
820
|
+
} = interestRateConfig;
|
|
821
|
+
|
|
822
|
+
const nextFullUtilizationRate = this.fullUtilizationRate(
|
|
823
|
+
interestRateConfig,
|
|
824
|
+
timeDelta,
|
|
825
|
+
scaledUtilization,
|
|
826
|
+
lastFullUtilizationRate
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const targetRate =
|
|
830
|
+
(((nextFullUtilizationRate - zero_utilization_rate) * target_rate_percent) / SCALE) +
|
|
831
|
+
zero_utilization_rate;
|
|
832
|
+
|
|
833
|
+
let newRatePerSecond: bigint;
|
|
834
|
+
|
|
835
|
+
if (scaledUtilization < target_utilization) {
|
|
836
|
+
newRatePerSecond =
|
|
837
|
+
zero_utilization_rate +
|
|
838
|
+
(scaledUtilization * (targetRate - zero_utilization_rate)) /
|
|
839
|
+
target_utilization;
|
|
840
|
+
} else {
|
|
841
|
+
newRatePerSecond =
|
|
842
|
+
targetRate +
|
|
843
|
+
((scaledUtilization - target_utilization) *
|
|
844
|
+
(nextFullUtilizationRate - targetRate)) /
|
|
845
|
+
(UTILIZATION_SCALE - target_utilization);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { newRatePerSecond, nextFullUtilizationRate };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Calculates utilization given a specific rate per second.
|
|
853
|
+
* This is an inverse function of the piecewise interest rate formula above.
|
|
854
|
+
*/
|
|
855
|
+
getMaxUtilizationGivenRatePerSecond(
|
|
856
|
+
interestRateConfig: InterestRateConfig,
|
|
857
|
+
ratePerSecond: bigint,
|
|
858
|
+
timeDelta: bigint,
|
|
859
|
+
last_full_utilization_rate: bigint
|
|
860
|
+
): bigint {
|
|
861
|
+
logger.verbose(`VesuAdapter::getMaxUtilizationGivenRatePerSecond ratePerSecond: ${Number(ratePerSecond) / 1e18}, timeDelta: ${Number(timeDelta) / 1e18}, last_full_utilization_rate: ${Number(last_full_utilization_rate) / 1e18}`);
|
|
862
|
+
// use binary search to find the max utilization
|
|
863
|
+
// vary utlisation from 0 to SCALE, answer is the utilisation where the next step > ratePerSecond and the previous step < ratePerSecond
|
|
864
|
+
// Step is vary by SCALE / 100
|
|
865
|
+
let utilization = 0n;
|
|
866
|
+
let nextUtilization = SCALE / 100n;
|
|
867
|
+
while (utilization <= SCALE) {
|
|
868
|
+
logger.verbose(`VesuAdapter::getMaxUtilizationGivenRatePerSecond utilization: ${Number(utilization) / 1e18}, nextUtilization: ${Number(nextUtilization) / 1e18}`);
|
|
869
|
+
const { newRatePerSecond } = this.calculateInterestRate(interestRateConfig, utilization, timeDelta, last_full_utilization_rate);
|
|
870
|
+
if (newRatePerSecond > ratePerSecond) {
|
|
871
|
+
return utilization;
|
|
872
|
+
}
|
|
873
|
+
utilization += nextUtilization;
|
|
874
|
+
}
|
|
875
|
+
throw new Error('Max utilization not found');
|
|
876
|
+
}
|
|
676
877
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FAQ, getNoRiskTags, highlightTextWithLinks, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType } from "@/interfaces";
|
|
1
|
+
import { FAQ, getNoRiskTags, highlightTextWithLinks, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
|
|
2
2
|
import { AUMTypes, getContractDetails, UNIVERSAL_ADAPTERS, UNIVERSAL_MANAGE_IDS, UniversalManageCall, UniversalStrategy, UniversalStrategySettings } from "./universal-strategy";
|
|
3
3
|
import { PricerBase } from "@/modules/pricerBase";
|
|
4
4
|
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
@@ -12,12 +12,15 @@ import { SingleTokenInfo } from "./base-strategy";
|
|
|
12
12
|
import { Call, Contract, uint256 } from "starknet";
|
|
13
13
|
import ERC4626Abi from "@/data/erc4626.abi.json";
|
|
14
14
|
|
|
15
|
+
export interface HyperLSTStrategySettings extends UniversalStrategySettings {
|
|
16
|
+
borrowable_assets: TokenInfo[];
|
|
17
|
+
}
|
|
15
18
|
|
|
16
|
-
export class UniversalLstMultiplierStrategy extends UniversalStrategy<
|
|
19
|
+
export class UniversalLstMultiplierStrategy extends UniversalStrategy<HyperLSTStrategySettings> {
|
|
17
20
|
|
|
18
21
|
private quoteAmountToFetchPrice = new Web3Number(1, 18);
|
|
19
22
|
|
|
20
|
-
constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<
|
|
23
|
+
constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<HyperLSTStrategySettings>) {
|
|
21
24
|
super(config, pricer, metadata);
|
|
22
25
|
|
|
23
26
|
const STRKToken = Global.getDefaultTokens().find(token => token.symbol === 'STRK')!;
|
|
@@ -39,14 +42,32 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
39
42
|
return `${UniversalLstMultiplierStrategy.name}:${this.metadata.name}`;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// Vesu adapter with LST and base token match
|
|
46
|
+
getVesuSameTokenAdapter() {
|
|
47
|
+
const baseAdapter = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG1) as VesuAdapter;
|
|
48
|
+
baseAdapter.networkConfig = this.config;
|
|
49
|
+
baseAdapter.pricer = this.pricer;
|
|
50
|
+
return baseAdapter;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
// only one leg is used
|
|
43
54
|
// todo support lending assets of underlying as well (like if xSTRK looping is not viable, simply supply STRK)
|
|
44
55
|
getVesuAdapters() {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const adapters: VesuAdapter[] = [];
|
|
57
|
+
const baseAdapter = this.getVesuSameTokenAdapter();
|
|
58
|
+
for (const asset of this.metadata.additionalInfo.borrowable_assets) {
|
|
59
|
+
const vesuAdapter1 = new VesuAdapter({
|
|
60
|
+
poolId: baseAdapter.config.poolId,
|
|
61
|
+
collateral: this.asset(),
|
|
62
|
+
debt: asset,
|
|
63
|
+
vaultAllocator: this.metadata.additionalInfo.vaultAllocator,
|
|
64
|
+
id: ''
|
|
65
|
+
})
|
|
66
|
+
vesuAdapter1.pricer = this.pricer;
|
|
67
|
+
vesuAdapter1.networkConfig = this.config;
|
|
68
|
+
adapters.push(vesuAdapter1);
|
|
69
|
+
}
|
|
70
|
+
return adapters;
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
// not applicable for this strategy
|
|
@@ -84,7 +105,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
84
105
|
// TODO use a varibale for 1.02
|
|
85
106
|
return this._getAvnuDepositSwapLegCall({
|
|
86
107
|
...params,
|
|
87
|
-
minHF: 1.
|
|
108
|
+
minHF: 1.1 // undo
|
|
88
109
|
});
|
|
89
110
|
}
|
|
90
111
|
|
|
@@ -123,6 +144,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
123
144
|
const totalDebtAmount = totalCollateral.multipliedBy(collateralPrice).multipliedBy(legLTV).dividedBy(debtPrice).dividedBy(params.minHF);
|
|
124
145
|
logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall totalDebtAmount: ${totalDebtAmount}`);
|
|
125
146
|
const debtAmount = totalDebtAmount.minus(existingDebtInfo.amount);
|
|
147
|
+
logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall debtAmount: ${debtAmount}`);
|
|
126
148
|
if (debtAmount.lt(0)) {
|
|
127
149
|
// this is to unwind the position to optimal HF.
|
|
128
150
|
const lstDEXPrice = await this.getLSTDexPrice();
|
|
@@ -131,10 +153,9 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
131
153
|
isDeposit: false,
|
|
132
154
|
leg1DepositAmount: debtAmountInLST
|
|
133
155
|
})
|
|
134
|
-
assert(calls.length == 1,
|
|
156
|
+
assert(calls.length == 1, `Expected 1 call for unwind, got ${calls.length}`);
|
|
135
157
|
return calls[0];
|
|
136
158
|
}
|
|
137
|
-
logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall debtAmount: ${debtAmount}`);
|
|
138
159
|
const STEP0 = UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1;
|
|
139
160
|
const manage0Info = this.getProofs<ApproveCallParams>(STEP0);
|
|
140
161
|
const manageCall0 = manage0Info.callConstructor({
|
|
@@ -261,11 +282,42 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
261
282
|
}
|
|
262
283
|
}
|
|
263
284
|
|
|
285
|
+
protected async getVesuAUM(adapter: VesuAdapter) {
|
|
286
|
+
const legAUM = await adapter.getPositions(this.config);
|
|
287
|
+
const underlying = this.asset();
|
|
288
|
+
// assert its an LST of Endur
|
|
289
|
+
assert(underlying.symbol.startsWith('x'), 'Underlying is not an LST of Endur');
|
|
290
|
+
|
|
291
|
+
let vesuAum = Web3Number.fromWei("0", underlying.decimals);
|
|
292
|
+
|
|
293
|
+
let tokenUnderlyingPrice = await this.getLSTExchangeRate();
|
|
294
|
+
// to offset for usual DEX lag, we multiply by 0.998 (i.e. 0.2% loss)
|
|
295
|
+
tokenUnderlyingPrice = tokenUnderlyingPrice * 0.998;
|
|
296
|
+
logger.verbose(`${this.getTag()} tokenUnderlyingPrice: ${tokenUnderlyingPrice}`);
|
|
297
|
+
|
|
298
|
+
// handle collateral
|
|
299
|
+
if (legAUM[0].token.address.eq(underlying.address)) {
|
|
300
|
+
vesuAum = vesuAum.plus(legAUM[0].amount);
|
|
301
|
+
} else {
|
|
302
|
+
vesuAum = vesuAum.plus(legAUM[0].amount.dividedBy(tokenUnderlyingPrice));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// handle debt
|
|
306
|
+
if (legAUM[1].token.address.eq(underlying.address)) {
|
|
307
|
+
vesuAum = vesuAum.minus(legAUM[1].amount);
|
|
308
|
+
} else {
|
|
309
|
+
vesuAum = vesuAum.minus(legAUM[1].amount.dividedBy(tokenUnderlyingPrice));
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
logger.verbose(`${this.getTag()} Vesu AUM: ${vesuAum}, legCollateral: ${legAUM[0].amount.toNumber()}, legDebt: ${legAUM[1].amount.toNumber()}`);
|
|
313
|
+
return vesuAum;
|
|
314
|
+
}
|
|
315
|
+
|
|
264
316
|
//
|
|
265
317
|
private async _getMinOutputAmountLSTBuy(amountInUnderlying: Web3Number) {
|
|
266
318
|
const lstTruePrice = await this.getLSTExchangeRate();
|
|
267
319
|
// during buy, the purchase should always be <= true LST price.
|
|
268
|
-
const minOutputAmount = amountInUnderlying.dividedBy(lstTruePrice);
|
|
320
|
+
const minOutputAmount = amountInUnderlying.dividedBy(lstTruePrice).multipliedBy(0.99979); // minus 0.021% to account for avnu fees
|
|
269
321
|
return minOutputAmount;
|
|
270
322
|
}
|
|
271
323
|
|
|
@@ -292,6 +344,18 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
292
344
|
const legLTV = await vesuAdapter1.getLTVConfig(this.config);
|
|
293
345
|
logger.verbose(`${this.getTag()}::getVesuMultiplyCall legLTV: ${legLTV}`);
|
|
294
346
|
|
|
347
|
+
if (!params.isDeposit) {
|
|
348
|
+
// try using unused balance to unwind.
|
|
349
|
+
// no need to unwind.
|
|
350
|
+
const unusedBalance = await this.getUnusedBalance();
|
|
351
|
+
logger.verbose(`${this.getTag()}::getVesuMultiplyCall unusedBalance: ${unusedBalance.amount.toString()}, required: ${params.leg1DepositAmount.toString()}`);
|
|
352
|
+
// undo
|
|
353
|
+
// if (unusedBalance.amount.gte(params.leg1DepositAmount)) {
|
|
354
|
+
// return [];
|
|
355
|
+
// }
|
|
356
|
+
// throw new Error('Unused balance is less than the amount to unwind');
|
|
357
|
+
}
|
|
358
|
+
|
|
295
359
|
const existingPositions = await vesuAdapter1.getPositions(this.config);
|
|
296
360
|
const collateralisation = await vesuAdapter1.getCollateralization(this.config);
|
|
297
361
|
const existingCollateralInfo = existingPositions[0];
|
|
@@ -350,17 +414,37 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
350
414
|
}
|
|
351
415
|
|
|
352
416
|
getLSTUnderlyingTokenInfo() {
|
|
353
|
-
const
|
|
417
|
+
const vesuAdapter1 = this.getVesuSameTokenAdapter();
|
|
354
418
|
return vesuAdapter1.config.debt;
|
|
355
419
|
}
|
|
356
420
|
|
|
421
|
+
async getMaxBorrowableAmount() {
|
|
422
|
+
const vesuAdapters = this.getVesuAdapters();
|
|
423
|
+
let netMaxBorrowableAmount = Web3Number.fromWei("0", this.getLSTUnderlyingTokenInfo().decimals);
|
|
424
|
+
const maxBorrowables: {amount: Web3Number, borrowableAsset: TokenInfo}[] = [];
|
|
425
|
+
const lstAPY = await this.getLSTAPR(this.getLSTUnderlyingTokenInfo().address);
|
|
426
|
+
const maxInterestRate = lstAPY * 0.8;
|
|
427
|
+
for (const vesuAdapter of vesuAdapters) {
|
|
428
|
+
const maxBorrowableAmount = await vesuAdapter.getMaxBorrowableByInterestRate(this.config, vesuAdapter.config.debt, maxInterestRate);
|
|
429
|
+
const debtCap = await vesuAdapter.getDebtCap(this.config);
|
|
430
|
+
maxBorrowables.push({amount: maxBorrowableAmount.minimum(debtCap), borrowableAsset: vesuAdapter.config.debt});
|
|
431
|
+
}
|
|
432
|
+
maxBorrowables.sort((a, b) => b.amount.toNumber() - a.amount.toNumber());
|
|
433
|
+
netMaxBorrowableAmount = maxBorrowables.reduce((acc, curr) => acc.plus(curr.amount), Web3Number.fromWei("0", this.getLSTUnderlyingTokenInfo().decimals));
|
|
434
|
+
return {netMaxBorrowableAmount, maxBorrowables};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// todo how much to unwind to get back healthy APY zone again
|
|
438
|
+
// if net APY < LST APR + 0.5%, we need to unwind to get back to LST APR + 1% atleast or 0 vesu position
|
|
439
|
+
// For xSTRK, simply deposit in Vesu if looping is not viable
|
|
440
|
+
|
|
357
441
|
/**
|
|
358
442
|
* Gets LST APR for the strategy's underlying asset from Endur API
|
|
359
443
|
* @returns Promise<number> The LST APR (not divided by 1e18)
|
|
360
444
|
*/
|
|
361
445
|
async getLSTAPR(_address: ContractAddr): Promise<number> {
|
|
362
446
|
try {
|
|
363
|
-
const vesuAdapter1 = this.
|
|
447
|
+
const vesuAdapter1 = this.getVesuSameTokenAdapter();
|
|
364
448
|
const apr = await LSTAPRService.getLSTAPR(vesuAdapter1.config.debt.address);
|
|
365
449
|
if (!apr) {
|
|
366
450
|
throw new Error('Failed to get LST APR');
|
|
@@ -374,22 +458,50 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
374
458
|
|
|
375
459
|
// todo undo this
|
|
376
460
|
async netAPY(): Promise<{ net: number; splits: { apy: number; id: string; }[]; }> {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
461
|
+
const unusedBalance = await this.getUnusedBalance();
|
|
462
|
+
const maxNewDeposits = await this.maxNewDeposits();
|
|
463
|
+
|
|
464
|
+
// if unused balance is > max servicable from loan, we are limited by the max borrowing we can do
|
|
465
|
+
// we also allow accepting little higher deposits (1.5x) to have room for future looping when theres more liquidity or debt cap rises
|
|
466
|
+
if (maxNewDeposits * 1.5 < unusedBalance.amount.toNumber()) {
|
|
467
|
+
// we have excess, just use real APY
|
|
468
|
+
return super.netAPY();
|
|
469
|
+
} else {
|
|
470
|
+
// we have little bit room to accept more deposits, we use theoretical max APY
|
|
471
|
+
const { positions, baseAPYs, rewardAPYs } = await this.getVesuAPYs();
|
|
472
|
+
const weights = positions.map((p, index) => p.usdValue * (index % 2 == 0 ? 1 : -1));
|
|
473
|
+
return this.returnNetAPY(baseAPYs, rewardAPYs, weights);
|
|
383
474
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async maxNewDeposits() {
|
|
478
|
+
const maxBorrowableAmounts = await this.getMaxBorrowableAmount();
|
|
479
|
+
|
|
480
|
+
let ltv: number | undefined = undefined;
|
|
481
|
+
for (let adapter of this.getVesuAdapters()) {
|
|
482
|
+
const maxBorrowableAmount = maxBorrowableAmounts.maxBorrowables.find(b => b.borrowableAsset.address.eq(adapter.config.debt.address))?.amount;
|
|
483
|
+
if (!maxBorrowableAmount) {
|
|
484
|
+
throw new Error(`Max borrowable amount not found for adapter: ${adapter.config.debt.symbol}`);
|
|
485
|
+
}
|
|
486
|
+
const maxLTV = await adapter.getLTVConfig(this.config);
|
|
487
|
+
if (!ltv) {
|
|
488
|
+
ltv = maxLTV;
|
|
489
|
+
} else if (ltv != maxLTV) {
|
|
490
|
+
throw new Error(`LTV mismatch for adapter: ${adapter.config.debt.symbol}`);
|
|
491
|
+
}
|
|
387
492
|
}
|
|
493
|
+
if (!ltv) {
|
|
494
|
+
throw new Error('LTV not found');
|
|
495
|
+
}
|
|
496
|
+
// for simplicity, we assume 1 underlying = 1 LST
|
|
497
|
+
const numerator = this.metadata.additionalInfo.targetHealthFactor * maxBorrowableAmounts.netMaxBorrowableAmount.toNumber() / (ltv)
|
|
498
|
+
return numerator - maxBorrowableAmounts.netMaxBorrowableAmount.toNumber();
|
|
388
499
|
}
|
|
389
500
|
|
|
501
|
+
// todo revisit cases where 0th adapters is used
|
|
390
502
|
protected async getUnusedBalanceAPY() {
|
|
391
503
|
const unusedBalance = await this.getUnusedBalance();
|
|
392
|
-
const vesuAdapter = this.
|
|
504
|
+
const vesuAdapter = this.getVesuSameTokenAdapter();
|
|
393
505
|
const underlying = vesuAdapter.config.debt;
|
|
394
506
|
const lstAPY = await this.getLSTAPR(underlying.address);
|
|
395
507
|
return {
|
|
@@ -398,7 +510,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
398
510
|
}
|
|
399
511
|
|
|
400
512
|
async getLSTExchangeRate() {
|
|
401
|
-
const
|
|
513
|
+
const vesuAdapter1 = this.getVesuSameTokenAdapter();
|
|
402
514
|
const lstTokenInfo = vesuAdapter1.config.collateral;
|
|
403
515
|
const lstABI = new Contract({
|
|
404
516
|
abi: ERC4626Abi,
|
|
@@ -425,7 +537,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
|
|
|
425
537
|
logger.verbose(`${this.getTag()}::getModifyLeverCall marginAmount: ${params.marginAmount}, debtAmount: ${params.debtAmount}, lstDexPriceInUnderlying: ${params.lstDexPriceInUnderlying}, isIncrease: ${params.isIncrease}`);
|
|
426
538
|
assert(!params.marginAmount.isZero() || !params.debtAmount.isZero(), 'Deposit/debt must be non-0');
|
|
427
539
|
|
|
428
|
-
const
|
|
540
|
+
const vesuAdapter1 = this.getVesuSameTokenAdapter();
|
|
429
541
|
const lstTokenInfo = this.asset();
|
|
430
542
|
const lstUnderlyingTokenInfo = vesuAdapter1.config.debt;
|
|
431
543
|
|
|
@@ -604,7 +716,7 @@ enum LST_MULTIPLIER_MANAGE_IDS {
|
|
|
604
716
|
function getLooperSettings(
|
|
605
717
|
lstSymbol: string,
|
|
606
718
|
underlyingSymbol: string,
|
|
607
|
-
vaultSettings:
|
|
719
|
+
vaultSettings: HyperLSTStrategySettings,
|
|
608
720
|
pool1: ContractAddr,
|
|
609
721
|
) {
|
|
610
722
|
vaultSettings.leafAdapters = [];
|
|
@@ -730,7 +842,11 @@ const _riskFactor: RiskFactor[] = [
|
|
|
730
842
|
{type: RiskType.DEPEG_RISK, value: DepegRiskLevel.GENERALLY_STABLE, weight: 25, reason: "Generally stable pegged assets" },
|
|
731
843
|
];
|
|
732
844
|
|
|
733
|
-
const
|
|
845
|
+
const borrowableAssets = [
|
|
846
|
+
'WBTC', 'tBTC', 'LBTC', 'solvBTC'
|
|
847
|
+
]
|
|
848
|
+
|
|
849
|
+
const hyperxSTRK: HyperLSTStrategySettings = {
|
|
734
850
|
vaultAddress: ContractAddr.from('0x46c7a54c82b1fe374353859f554a40b8bd31d3e30f742901579e7b57b1b5960'),
|
|
735
851
|
manager: ContractAddr.from('0x5d499cd333757f461a0bedaca3dfc4d77320c773037e0aa299f22a6dbfdc03a'),
|
|
736
852
|
vaultAllocator: ContractAddr.from('0x511d07953a09bc7c505970891507c5a2486d2ea22752601a14db092186d7caa'),
|
|
@@ -739,10 +855,11 @@ const hyperxSTRK: UniversalStrategySettings = {
|
|
|
739
855
|
leafAdapters: [],
|
|
740
856
|
adapters: [],
|
|
741
857
|
targetHealthFactor: 1.1,
|
|
742
|
-
minHealthFactor: 1.05
|
|
858
|
+
minHealthFactor: 1.05,
|
|
859
|
+
borrowable_assets: Global.getDefaultTokens().filter(token => token.symbol === 'STRK'),
|
|
743
860
|
}
|
|
744
861
|
|
|
745
|
-
const hyperxWBTC:
|
|
862
|
+
const hyperxWBTC: HyperLSTStrategySettings = {
|
|
746
863
|
vaultAddress: ContractAddr.from('0x2da9d0f96a46b453f55604313785dc866424240b1c6811d13bef594343db818'),
|
|
747
864
|
manager: ContractAddr.from('0x75866db44c81e6986f06035206ee9c7d15833ddb22d6a22c016cfb5c866a491'),
|
|
748
865
|
vaultAllocator: ContractAddr.from('0x57b5c1bb457b5e840a2714ae53ada87d77be2f3fd33a59b4fe709ef20c020c1'),
|
|
@@ -751,10 +868,11 @@ const hyperxWBTC: UniversalStrategySettings = {
|
|
|
751
868
|
leafAdapters: [],
|
|
752
869
|
adapters: [],
|
|
753
870
|
targetHealthFactor: 1.1,
|
|
754
|
-
minHealthFactor: 1.05
|
|
871
|
+
minHealthFactor: 1.05,
|
|
872
|
+
borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
|
|
755
873
|
}
|
|
756
874
|
|
|
757
|
-
const hyperxtBTC:
|
|
875
|
+
const hyperxtBTC: HyperLSTStrategySettings = {
|
|
758
876
|
vaultAddress: ContractAddr.from('0x47d5f68477e5637ce0e56436c6b5eee5a354e6828995dae106b11a48679328'),
|
|
759
877
|
manager: ContractAddr.from('0xc4cc3e08029a0ae076f5fdfca70575abb78d23c5cd1c49a957f7e697885401'),
|
|
760
878
|
vaultAllocator: ContractAddr.from('0x50bbd4fe69f841ecb13b2619fe50ebfa4e8944671b5d0ebf7868fd80c61b31e'),
|
|
@@ -763,10 +881,11 @@ const hyperxtBTC: UniversalStrategySettings = {
|
|
|
763
881
|
leafAdapters: [],
|
|
764
882
|
adapters: [],
|
|
765
883
|
targetHealthFactor: 1.1,
|
|
766
|
-
minHealthFactor: 1.05
|
|
884
|
+
minHealthFactor: 1.05,
|
|
885
|
+
borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
|
|
767
886
|
}
|
|
768
887
|
|
|
769
|
-
const hyperxsBTC:
|
|
888
|
+
const hyperxsBTC: HyperLSTStrategySettings = {
|
|
770
889
|
vaultAddress: ContractAddr.from('0x437ef1e7d0f100b2e070b7a65cafec0b2be31b0290776da8b4112f5473d8d9'),
|
|
771
890
|
manager: ContractAddr.from('0xc9ac023090625b0be3f6532ca353f086746f9c09f939dbc1b2613f09e5f821'),
|
|
772
891
|
vaultAllocator: ContractAddr.from('0x60c2d856936b975459a5b4eb28b8672d91f757bd76cebb6241f8d670185dc01'),
|
|
@@ -775,10 +894,11 @@ const hyperxsBTC: UniversalStrategySettings = {
|
|
|
775
894
|
leafAdapters: [],
|
|
776
895
|
adapters: [],
|
|
777
896
|
targetHealthFactor: 1.1,
|
|
778
|
-
minHealthFactor: 1.05
|
|
897
|
+
minHealthFactor: 1.05,
|
|
898
|
+
borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
|
|
779
899
|
}
|
|
780
900
|
|
|
781
|
-
const hyperxLBTC:
|
|
901
|
+
const hyperxLBTC: HyperLSTStrategySettings = {
|
|
782
902
|
vaultAddress: ContractAddr.from('0x64cf24d4883fe569926419a0569ab34497c6956a1a308fa883257f7486d7030'),
|
|
783
903
|
manager: ContractAddr.from('0x203530a4022a99b8f4b406aaf33b0849d43ad7422c1d5cc14ff8c667abec6c0'),
|
|
784
904
|
vaultAllocator: ContractAddr.from('0x7dbc8ccd4eabce6ea6c19e0e5c9ccca3a93bd510303b9e071cbe25fc508546e'),
|
|
@@ -787,7 +907,8 @@ const hyperxLBTC: UniversalStrategySettings = {
|
|
|
787
907
|
leafAdapters: [],
|
|
788
908
|
adapters: [],
|
|
789
909
|
targetHealthFactor: 1.1,
|
|
790
|
-
minHealthFactor: 1.05
|
|
910
|
+
minHealthFactor: 1.05,
|
|
911
|
+
borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
|
|
791
912
|
}
|
|
792
913
|
|
|
793
914
|
function getInvestmentSteps(lstSymbol: string, underlyingSymbol: string) {
|
|
@@ -800,7 +921,7 @@ function getInvestmentSteps(lstSymbol: string, underlyingSymbol: string) {
|
|
|
800
921
|
]
|
|
801
922
|
}
|
|
802
923
|
|
|
803
|
-
function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addresses:
|
|
924
|
+
function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addresses: HyperLSTStrategySettings, isPreview: boolean = false): IStrategyMetadata<HyperLSTStrategySettings> {
|
|
804
925
|
return {
|
|
805
926
|
name: `Hyper ${lstSymbol}`,
|
|
806
927
|
description: getDescription(lstSymbol, underlyingSymbol),
|
|
@@ -822,11 +943,12 @@ function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addres
|
|
|
822
943
|
contractDetails: getContractDetails(addresses),
|
|
823
944
|
faqs: getFAQs(lstSymbol, underlyingSymbol),
|
|
824
945
|
investmentSteps: getInvestmentSteps(lstSymbol, underlyingSymbol),
|
|
825
|
-
isPreview: isPreview
|
|
946
|
+
isPreview: isPreview,
|
|
947
|
+
apyMethodology: 'Current annualized APY in terms of base asset of the LST'
|
|
826
948
|
}
|
|
827
949
|
}
|
|
828
950
|
|
|
829
|
-
export const HyperLSTStrategies: IStrategyMetadata<
|
|
951
|
+
export const HyperLSTStrategies: IStrategyMetadata<HyperLSTStrategySettings>[] =
|
|
830
952
|
[
|
|
831
953
|
getStrategySettings('xSTRK', 'STRK', hyperxSTRK, false),
|
|
832
954
|
getStrategySettings('xWBTC', 'WBTC', hyperxWBTC, false),
|