diff --git a/README.md b/README.md index ecd0094..9f2582c 100644 --- a/README.md +++ b/README.md @@ -947,7 +947,457 @@ await lendMarket.forceUpdateUserState(txHash, "0x123..."); })() ``` -### Leverage (createLoan, borrowMore, repay) for lendMarket +### Leverage (createLoan, borrowMore, repay) for lendMarket leverageZapV2 + +LendMarket leverage operations support different routers and require quote information from external routing services. + +#### Breaking Changes + +**1. New metrics methods - get all metrics in one call** +- Instead of `createLoanBands`, `createLoanPrices`, `createLoanHealth`, `createLoanPriceImpact`: `createLoanExpectedMetrics()` - returns priceImpact, bands, prices, health +- Instead of `borrowMoreBands`, `borrowMorePrices`, `borrowMoreHealth`, `borrowMorePriceImpact`: + `borrowMoreExpectedMetrics()` – returns priceImpact, bands, prices, health +- Instead of `repayBands`, `repayPrices`, `repayHealth`, `repayPriceImpact`: + `repayExpectedMetrics()` – returns priceImpact, bands, prices, health + + +**ILeverageMetrics** - metrics object returned by new methods: + +```ts +interface ILeverageMetrics { + priceImpact: number; + bands: [number, number]; + prices: string[]; + health: string; +} +``` + +**2. Final methods now require `router` and `calldata` parameters** +- **Before**: `createLoan(userCollateral, userBorrowed, debt, range)` +- **Now**: `createLoan(userCollateral, userBorrowed, debt, range, router, calldata)` +- **Before**: `borrowMore(userCollateral, userBorrowed, debt, slippage)` +- **Now**: `borrowMore(userCollateral, userBorrowed, debt, router, calldata)` +- **Before**: `repay(stateCollateral, userCollateral, userBorrowed, slippage)` +- **Now**: `repay(stateCollateral, userCollateral, userBorrowed, router, calldata)` + +where `router` - address of router, `calldata` - calldata byte code + +#### Understanding Callback Functions and Quotes + +**GetExpectedFn** - callback function for fetching quotes from router (e.g., Odos, Curve): + +```ts +type GetExpectedFn = ( + fromToken: string, // Address of token to swap from + toToken: string, // Address of token to swap to + amountIn: bigint, // Amount of tokens to swap (in wei) + blacklist: string | string[], // Contract/pool addresses to exclude +) => Promise; +``` + +**IQuote** - quote object with swap information: + +```ts +interface IQuote { + outAmount: string; // Amount of tokens to receive + priceImpact: number; // Price impac +} +``` + +#### Which quotes are needed for different operations + +| Action type | Quote inputAmount | +|--------------|------------------| +| createLoan | debt + userBorrowed | +| borrowMore | debt + userBorrowed | +| repay | stateCollateral + userCollateral | + +**For createLoan and borrowMore:** +- Get quote for swapping `(debt + userBorrowed)` borrowed tokens → collateral tokens +- Example: For debt=2000 and userBorrowed=1000, get quote for swapping 3000 borrowed tokens to collateral + +!!! Be careful: here debt means the amount of debt added during the operation, not the new total debt the user will have after it. + +**For repay:** +- Get quote for swapping `(stateCollateral + userCollateral)` collateral tokens → borrowed tokens +- Example: For stateCollateral=2 and userCollateral=1, get quote for swapping 3 collateral tokens to borrowed tokens + + +#### Leverage operations with routers + +```ts +(async () => { + await llamalend.init('JsonRpc', {}); + await llamalend.lendMarkets.fetchMarkets(); + + const lendMarket = llamalend.getLendMarket('one-way-market-0'); + + const router = '0x.....'; // router name + const calldata = '0x...'; // calldata from router for executing swap + const quote = // quote from your router (debt + userBorrowed) + + // - Create Loan - + + // Creates leveraged position (userCollateral + collateralFromUserBorrowed + leverage_collateral) + // ^ + // | + // userCollateral | debt debt + userBorrowed + // user ---> controller ----> leverage_zap ----> router + // | ^ | ^ ^ | + // | |__________________| | |___________________| + // | leverageCollateral + collateralFromUserBorrowed + // |_____________________________________| + // userBorrowed + + let userCollateral = 1; + let userBorrowed = 1000; + let debt = 2000; + const range = 10; + await lendMarket.leverageZapV2.maxLeverage(range); + // 7.4728229145282742179 + + // Get maximum possible debt for given parameters + await lendMarket.leverageZapV2.createLoanMaxRecv({ userCollateral, userBorrowed, range, getExpected }); + // { + // maxDebt: '26089.494406081862861214', + // maxTotalCollateral: '9.539182089833411347', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315221168834966496', + // collateralFromMaxDebt: '8.223960920998444851', + // maxLeverage: '7.25291100528992828612', + // avgPrice: '3172.3757757003568790858' + // } + + // Get quote for swapping (debt + userBorrowed)!!! + + // Get expected collateral amount + await lendMarket.leverageZapV2.createLoanExpectedCollateral({ userCollateral, userBorrowed, debt, quote }); + // { + // totalCollateral: '1.946422996710829', + // userCollateral: '1.0', + // collateralFromUserBorrowed: '0.315474332236942984', + // collateralFromDebt: '0.630948664473886', + // leverage: '1.4796358613861877' + // avgPrice: '3169.8299919022623523421' + // } + + // NEW: Get all metrics in one call + const metrics = await lendMarket.leverageZapV2.createLoanExpectedMetrics({ + userCollateral, + userBorrowed, + debt, + range, + quote, + healthIsFull: true // true for full health, false for not full + }); + // { + // priceImpact: 0.08944411854377342, // % + // bands: [76, 67], + // prices: ['1027.977701011670136614', '1187.061409925215211173'], + // health: '195.8994783042570637' // % + // + + await lendMarket.leverageZapV2.createLoanIsApproved({ userCollateral, userBorrowed }); + // false + await lendMarket.leverageZapV2.createLoanApprove({ userCollateral, userBorrowed }); + // [ + // '0xd5491d9f1e9d8ac84b03867494e35b25efad151c597d2fa4211d7bf5d540c98e', + // '0x93565f37ec5be902a824714a30bddc25cf9cd9ed39b4c0e8de61fab44af5bc8c' + // ] + + + // Create loan, passing router address and calldata from router + await lendMarket.leverageZapV2.createLoan({ userCollateral, userBorrowed, debt, range, router, calldata }); + // 0xeb1b7a92bcb02598f00dc8bbfe8fa3a554e7a2b1ca764e0ee45e2bf583edf731 + + await lendMarket.wallet.balances(); + // { + // collateral: '99.0', + // borrowed: '599000.0', + // vaultShares: '1400000000.0', + // gauge: '0' + // } + await lendMarket.userState(); + // { + // collateral: '1.945616160868693648', + // borrowed: '0.0', + // debt: '2000.0', + // N: '10' + // } + await lendMarket.userBands(); + // [ 76, 67 ] + await lendMarket.userPrices(); + // [ '1027.977718614028011906', '1187.061430251609195098' ] + await lendMarket.userHealth(); + // 195.8372633833293605 + await lendMarket.userHealth(false); + // 3.2518122092914609 + + + // - Borrow More - + + // Updates leveraged position (dCollateral = userCollateral + collateralFromUserBorrowed + leverageCollateral) + // ^ + // | + // userCollateral | dDebt dDebt + userBorrowed + // user ---> controller ----> leverage_zap ----> router + // | ^ | ^ ^ | + // | |__________________| | |___________________| + // | leverageCollateral + collateralFromUSerBorrowed + // |_____________________________________| + // userBorrowed + + userCollateral = 2; + userBorrowed = 2000; + debt = 10000; + + // Get maximum possible debt for given parameters + await lendMarket.leverageZapV2.borrowMoreMaxRecv({ userCollateral, userBorrowed, getExpected }); + // { + // maxDebt: '76182.8497941193262889', + // maxTotalCollateral: '26.639775583730298462', + // userCollateral: '2', + // collateralFromUserBorrowed: '1.677318306610359627', + // collateralFromMaxDebt: '22.962457277119938834', + // avgPrice: '3172.55402418338331369083' + // } + + // Get quote for swapping (debt + userBorrowed) + + await lendMarket.leverageZapV2.borrowMoreExpectedCollateral({ userCollateral, userBorrowed, dDebt: debt, quote }); + // { + // totalCollateral: '5.783452104143246413', + // userCollateral: '2.0', + // collateralFromUserBorrowed: '0.630575350690541071', + // collateralFromDebt: '3.152876753452705342' + // avgPrice: '3171.70659749038129067231' + // } + + // NEW: Get all metrics in one call + const metricsBM = await lendMarket.leverageZapV2.borrowMoreExpectedMetrics({ + userCollateral, + userBorrowed, + debt, + quote, + healthIsFull: true // true for full health, false for not full + }); + // { + // priceImpact: 0.010784277354269765, // % + // bands: [47, 38], + // prices: ['1560.282474721398939216', '1801.742501325928269008'], + // health: '91.6798951784708552' // % + // } + + await lendMarket.leverageZapV2.borrowMoreIsApproved({ userCollateral, userBorrowed }); + // true + await lendMarket.leverageZapV2.borrowMoreApprove({ userCollateral, userBorrowed }); + // [] + + // Execute borrowMore, passing router address and calldata from router + await lendMarket.leverageZapV2.borrowMore({ userCollateral, userBorrowed, debt, router, calldata }); + // 0x6357dd6ea7250d7adb2344cd9295f8255fd8fbbe85f00120fbcd1ebf139e057c + + await lendMarket.wallet.balances(); + // { + // collateral: '97.0', + // borrowed: '597000.0', + // vaultShares: '1400000000.0', + // gauge: '0' + // } + await lendMarket.userState(); + // { + // collateral: '7.727839965845165558', + // borrowed: '0.0', + // debt: '12000.000010193901375446', + // N: '10' + // } + await lendMarket.userBands(); + // [ 47, 38 ] + await lendMarket.userPrices(); + // [ '1560.28248267408177179', '1801.742510509320950242' ] + await lendMarket.userHealth(); + // 91.6519475547753288 + await lendMarket.userHealth(false); + // 3.7449386373872907 + + + // - Repay - + + + // Deleveraged position (-dDebt = borrowedFromStateCollateral + borrowedFromUSerCollateral + userBorrowed) + // ^ + // | userCollateral + // user ___|__________________________ + // | | + // | | stateCollateral ↓ userCollateral + stateCollateral + // | controller --> leverage_zap --> router + // | ^ | ^ ^ | + // | |______________________| | |___________________| + // | | borrowedFromStateCollateral + // |________________________________| + + // userBorrowed borrowedFromUSerCollateral + + const stateCollateral = 2; + userCollateral = 1; + userBorrowed = 1500; + + // Get quote for swapping (stateCollateral + userCollateral) + + await lendMarket.leverageZapV2.repayExpectedBorrowed({ stateCollateral, userCollateral, userBorrowed, quote }); + // { + // totalBorrowed: '10998.882838599741571472', + // borrowedFromStateCollateral: '6332.588559066494374648', + // borrowedFromUserCollateral: '3166.294279533247196824', + // userBorrowed: '1500' + // avgPrice: '3166.29427953324743125312' + // } + + await lendMarket.leverageZapV2.repayIsFull({ stateCollateral, userCollateral, userBorrowed, quote }); + // false + await lendMarket.leverageZapV2.repayIsAvailable({ stateCollateral, userCollateral, userBorrowed, quote }); + // true + + // NEW: Get all metrics in one call + const metricsRepay = await lendMarket.leverageZapV2.repayExpectedMetrics({ + stateCollateral, + userCollateral, + userBorrowed, + healthIsFull: true, // true for full health, false for not full + quote, + address: llamalend.signerAddress + }); + // { + // priceImpact: 0.013150142802201724, // % + // bands: [199, 190], + // prices: ['175.130965754280721633', '202.233191367561902757'], + // health: '1699.6097751079226865' // % + // } + + await lendMarket.leverageZapV2.repayIsApproved({ userCollateral, userBorrowed }); + // false + await lendMarket.leverageZapV2.repayApprove({ userCollateral, userBorrowed }); + // ['0xd8a8d3b3f67395e1a4f4d4f95b041edcaf1c9f7bab5eb8a8a767467678295498'] + + // Execute repay, passing router address and calldata from router + await lendMarket.leverageZapV2.repay({ stateCollateral, userCollateral, userBorrowed, router, calldata }); + // 0xe48a97fef1c54180a2c7d104d210a95ac1a516fdd22109682179f1582da23a82 + + await lendMarket.wallet.balances(); + // { + // collateral: '96.0', + // borrowed: '595500.0', + // vaultShares: '1400000000.0', + // gauge: '0' + // } + await lendMarket.userState(); + // { + // collateral: '5.727839965845165558', + // borrowed: '0.0', + // debt: '992.083214663467727334', + // N: '10' + // } + await lendMarket.userBands(); + // [ 199, 190 ] + await lendMarket.userPrices(); + // [ '175.13096689602455189', '202.233192685995210783' ] + await lendMarket.userHealth(); + // 1716.0249924305707883 + await lendMarket.userHealth(false); + // 3.6389352509210336 +})() +``` + +### Leverage createLoan all ranges methods for lendMarket +```ts + await llamalend.init('JsonRpc', {}); + await llamalend.lendMarkets.fetchMarkets(); + + const lendMarket = llamalend.getLendMarket('one-way-market-0'); + + const userCollateral = 1; + const userBorrowed = 1000; + const debt = 2000; + + // Get maximum values for all possible ranges + await lendMarket.leverageZapV2.createLoanMaxRecvAllRanges({ userCollateral, userBorrowed, getExpected }); + // { + // '4': { + // maxDebt: '37916.338071504823875251', + // maxTotalCollateral: '13.286983617364703479', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315728154966395280', + // collateralFromMaxDebt: '11.971255462398308199', + // maxLeverage: '10.09857816541446843865', + // avgPrice: '3167.28167656266072703689' + // }, + // '5': { + // maxDebt: '35363.440522143354729759', + // maxTotalCollateral: '12.480961984286574804', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315728154966395280', + // collateralFromMaxDebt: '11.165233829320179524', + // maxLeverage: '9.48597317551918486951', + // avgPrice: '3167.28167656266072703689' + // }, + // '6': { + // maxDebt: '33122.824118147617102062', + // maxTotalCollateral: '11.773536301065561222', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315728154966395280', + // collateralFromMaxDebt: '10.457808146099165942', + // maxLeverage: '8.94830459971897955699', + // avgPrice: '3167.28167656266072703689' + // }, + // '7': { + // maxDebt: '31140.555201395785060968', + // maxTotalCollateral: '11.147678193332270290', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315728154966395280', + // collateralFromMaxDebt: '9.831950038365875010', + // maxLeverage: '8.47263027035929823721', + // avgPrice: '3167.28167656266072703689' + // }, + // + // ... + // + // '50': { + // maxDebt: '8122.705063645852013929', + // maxTotalCollateral: '3.880294838047496482', + // userCollateral: '1', + // collateralFromUserBorrowed: '0.315728154966395280', + // collateralFromMaxDebt: '2.564566683081101202', + // maxLeverage: '2.94916151440614435181', + // avgPrice: '3167.28167656266072703689' + // } + + // Get quote for specific debt + userBorrowed + // and provide getExpected callback + await lendMarket.leverageZapV2.createLoanBandsAllRanges({ userCollateral, userBorrowed, debt, getExpected, quote }); + // { + // '4': [ 73, 70 ], + // '5': [ 73, 69 ], + // '6': [ 74, 69 ], + // '7': [ 74, 68 ], + // + // ... + // + // '50': [ 97, 48 ] + // } + + await lendMarket.leverageZapV2.createLoanPricesAllRanges({ userCollateral, userBorrowed, debt, getExpected, quote }); + // { + // '4': [ '1073.323292757532604807', '1136.910693647788699808' ], + // '5': [ '1073.323292757532604807', '1153.387660222394333133' ], + // '6': [ '1057.990102860996424743', '1153.387660222394333133' ], + // '7': [ '1057.990102860996424743', '1170.103423414023236507' ], + // + // ... + // + // '50': [ '759.898822708156242647', '1560.282492846180089068' ] + // } +``` + +### Leverage (createLoan, borrowMore, repay) for lendMarket (Deprecated!!! Will be deleted in the future, please use leverageZapV2) ```ts (async () => { await llamalend.init('JsonRpc', {}, {}, API_KEY_1INCH); diff --git a/package.json b/package.json index e25bb1c..f9ea56d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@curvefi/llamalend-api", - "version": "1.0.42", + "version": "1.1.0", "description": "JavaScript library for Curve Lending", "main": "lib/index.js", "author": "Macket", @@ -16,6 +16,7 @@ "scripts": { "build": "rm -rf lib && tsc --project tsconfig.build.json", "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", "watch": "tsc --watch", "watch:lib": "rm -rf lib && tsc --watch --project tsconfig.build.json" }, diff --git a/src/cache/index.ts b/src/cache/index.ts index d42d1c9..0970130 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -1,5 +1,5 @@ class Cache { - // eslint-disable-next-line no-use-before-define + private static instance: Cache; readonly cache: Map; diff --git a/src/constants/aliases.ts b/src/constants/aliases.ts index c2bce93..9cffb7e 100644 --- a/src/constants/aliases.ts +++ b/src/constants/aliases.ts @@ -9,6 +9,7 @@ export const ALIASES_ETHEREUM = lowerCaseValues({ "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", // "leverage_zap": "0x3294514B78Df4Bb90132567fcf8E5e99f390B687", // 1inch "leverage_zap": "0xC5898606BdB494a994578453B92e7910a90aA873", // odos + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", "leverage_markets_start_id": "9", "crvUSD": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", "st_crvUSD": "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367", @@ -20,6 +21,7 @@ export const ALIASES_POLYGON = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_FANTOM = lowerCaseValues({ @@ -28,6 +30,7 @@ export const ALIASES_FANTOM = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_AVALANCHE = lowerCaseValues({ @@ -36,6 +39,7 @@ export const ALIASES_AVALANCHE = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_ARBITRUM = lowerCaseValues({ @@ -45,6 +49,7 @@ export const ALIASES_ARBITRUM = lowerCaseValues({ "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", // "leverage_zap": "0x61C404B60ee9c5fB09F70F9A645DD38fE5b3A956", // 1inch "leverage_zap": "0xb7b240CFa985306563A301bC417Bc9715059a117", // odos + "leverage_zap_v2": "0x5b07Db9a85992c877b9fBeA6DCC4F79292577640", "leverage_markets_start_id": "9", }); @@ -55,6 +60,7 @@ export const ALIASES_OPTIMISM = lowerCaseValues({ "gauge_factory_old": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "gauge_factory": "0x871fBD4E01012e2E8457346059e8C189d664DbA4", "leverage_zap": "0x273e44B9a1841857d9360e8792bB59f9e1FfE9Da", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", "leverage_markets_start_id": "0", "gas_oracle": '0xc0d3C0d3C0d3c0D3C0D3C0d3C0d3C0D3C0D3000f', "gas_oracle_blob": '0x420000000000000000000000000000000000000f', @@ -66,6 +72,7 @@ export const ALIASES_XDAI = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_MOONBEAM = lowerCaseValues({ @@ -74,6 +81,7 @@ export const ALIASES_MOONBEAM = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_AURORA = lowerCaseValues({ @@ -89,6 +97,7 @@ export const ALIASES_KAVA = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_CELO = lowerCaseValues({ @@ -97,6 +106,7 @@ export const ALIASES_CELO = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_ZKSYNC = lowerCaseValues({ @@ -105,6 +115,7 @@ export const ALIASES_ZKSYNC = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", // <--- TODO CHANGE "gauge_factory": "0x0000000000000000000000000000000000000000", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_BASE = lowerCaseValues({ @@ -113,6 +124,7 @@ export const ALIASES_BASE = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", // <--- TODO CHANGE "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_BSC = lowerCaseValues({ @@ -121,6 +133,7 @@ export const ALIASES_BSC = lowerCaseValues({ "gauge_controller": "0x0000000000000000000000000000000000000000", // <--- TODO CHANGE "gauge_factory": "0xabC000d88f23Bb45525E447528DBF656A9D55bf5", "leverage_zap": "0x0000000000000000000000000000000000000000", + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", }); export const ALIASES_FRAXTAL = lowerCaseValues({ @@ -130,6 +143,7 @@ export const ALIASES_FRAXTAL = lowerCaseValues({ "gauge_factory_old": "0xeF672bD94913CB6f1d2812a6e18c1fFdEd8eFf5c", "gauge_factory": "0x0b8d6b6cefc7aa1c2852442e518443b1b22e1c52", "leverage_zap": "0x37c5ab57AF7100Bdc9B668d766e193CCbF6614FD", // odos + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", "leverage_markets_start_id": "0", }); @@ -139,5 +153,6 @@ export const ALIASES_SONIC = lowerCaseValues({ "gauge_controller": "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", "gauge_factory": "0xf3A431008396df8A8b2DF492C913706BDB0874ef", "leverage_zap": "0x5552b631e2aD801fAa129Aacf4B701071cC9D1f7", // odos + "leverage_zap_v2": "0x0000000000000000000000000000000000000000", "leverage_markets_start_id": "0", }); \ No newline at end of file diff --git a/src/external-api.ts b/src/external-api.ts index 4c9aad6..4fdd414 100644 --- a/src/external-api.ts +++ b/src/external-api.ts @@ -118,9 +118,9 @@ export const _getUserCollateral = memoize( ) export const _getUserCollateralForce = async ( - network: INetworkName, - controller: string, - user: string, + network: INetworkName, + controller: string, + user: string, newTx: string ): Promise => { await fetch(`https://prices.curve.finance/v1/lending/collateral_events/${network}/${controller}/${user}?new_tx=${newTx}`); @@ -234,4 +234,4 @@ async function fetchJson(url: string): Promise { async function fetchData(url: string) { const {data} = await fetchJson(url); return data; -} +} \ No newline at end of file diff --git a/src/interfaces.ts b/src/interfaces.ts index ba9081f..6a2a429 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -230,3 +230,22 @@ export interface ILlamma { export interface IResponseApi { data: any } + +export interface IQuote { + outAmount: string, + priceImpact: number +} + +export interface ILeverageMetrics { + priceImpact: number, + bands: [number, number], + prices: string[], + health: string, +} + +export type GetExpectedFn = ( + fromToken: string, + toToken: string, + amountIn: bigint, + blacklist: string | string[], +) => Promise; \ No newline at end of file diff --git a/src/lendMarkets/LendMarketTemplate.ts b/src/lendMarkets/LendMarketTemplate.ts index 005eaff..1e18970 100644 --- a/src/lendMarkets/LendMarketTemplate.ts +++ b/src/lendMarkets/LendMarketTemplate.ts @@ -27,6 +27,8 @@ import {IDict, TGas, TAmount, IReward, IQuoteOdos, IOneWayMarket, IPartialFrac} import { _getExpectedOdos, _getQuoteOdos, _assembleTxOdos, _getUserCollateral, _getUserCollateralForce, _getMarketsData } from "../external-api.js"; import ERC20Abi from '../constants/abis/ERC20.json' with {type: 'json'}; import {cacheKey, cacheStats} from "../cache/index.js"; +import {ILeverageZapV2} from "./interfaces/leverageZapV2.js"; +import {LeverageZapV2Module} from "./modules/leverageZapV2.js"; const DAY = 86400; @@ -237,6 +239,7 @@ export class LendMarketTemplate { repay: (stateCollateral: TAmount, userCollateral: TAmount, userBorrowed: TAmount, slippage?: number) => Promise, } }; + leverageZapV2: ILeverageZapV2; constructor(id: string, marketData: IOneWayMarket, llamalend: Llamalend) { this.llamalend = llamalend; @@ -387,6 +390,51 @@ export class LendMarketTemplate { }, } + const leverageZapV2 = new LeverageZapV2Module(this); + + this.leverageZapV2 = { + hasLeverage: leverageZapV2.hasLeverage.bind(leverageZapV2), + + maxLeverage: leverageZapV2.maxLeverage.bind(leverageZapV2), + + createLoanMaxRecv: leverageZapV2.leverageCreateLoanMaxRecv.bind(leverageZapV2), + createLoanMaxRecvAllRanges: leverageZapV2.leverageCreateLoanMaxRecvAllRanges.bind(leverageZapV2), + createLoanExpectedCollateral: leverageZapV2.leverageCreateLoanExpectedCollateral.bind(leverageZapV2), + createLoanMaxRange: leverageZapV2.leverageCreateLoanMaxRange.bind(leverageZapV2), + createLoanBandsAllRanges: leverageZapV2.leverageCreateLoanBandsAllRanges.bind(leverageZapV2), + createLoanPricesAllRanges: leverageZapV2.leverageCreateLoanPricesAllRanges.bind(leverageZapV2), + createLoanIsApproved: leverageZapV2.leverageCreateLoanIsApproved.bind(leverageZapV2), + createLoanApprove: leverageZapV2.leverageCreateLoanApprove.bind(leverageZapV2), + createLoanExpectedMetrics: leverageZapV2.leverageCreateLoanExpectedMetrics.bind(leverageZapV2), + createLoan: leverageZapV2.leverageCreateLoan.bind(leverageZapV2), + + borrowMoreMaxRecv: leverageZapV2.leverageBorrowMoreMaxRecv.bind(leverageZapV2), + borrowMoreExpectedCollateral: leverageZapV2.leverageBorrowMoreExpectedCollateral.bind(leverageZapV2), + borrowMoreIsApproved: leverageZapV2.leverageCreateLoanIsApproved.bind(leverageZapV2), + borrowMoreApprove: leverageZapV2.leverageCreateLoanApprove.bind(leverageZapV2), + borrowMoreExpectedMetrics: leverageZapV2.leverageBorrowMoreExpectedMetrics.bind(leverageZapV2), + borrowMore: leverageZapV2.leverageBorrowMore.bind(leverageZapV2), + + repayExpectedBorrowed: leverageZapV2.leverageRepayExpectedBorrowed.bind(leverageZapV2), + repayIsFull: leverageZapV2.leverageRepayIsFull.bind(leverageZapV2), + repayIsAvailable: leverageZapV2.leverageRepayIsAvailable.bind(leverageZapV2), + repayExpectedMetrics: leverageZapV2.leverageRepayExpectedMetrics.bind(leverageZapV2), + repayIsApproved: leverageZapV2.leverageRepayIsApproved.bind(leverageZapV2), + repayApprove: leverageZapV2.leverageRepayApprove.bind(leverageZapV2), + repay: leverageZapV2.leverageRepay.bind(leverageZapV2), + + estimateGas: { + createLoanApprove: leverageZapV2.leverageCreateLoanApproveEstimateGas.bind(leverageZapV2), + createLoan: leverageZapV2.leverageCreateLoanEstimateGas.bind(leverageZapV2), + + borrowMoreApprove: leverageZapV2.leverageCreateLoanApproveEstimateGas.bind(leverageZapV2), + borrowMore: leverageZapV2.leverageBorrowMoreEstimateGas.bind(leverageZapV2), + + repayApprove: leverageZapV2.leverageRepayApproveEstimateGas.bind(leverageZapV2), + repay: leverageZapV2.leverageRepayEstimateGas.bind(leverageZapV2), + }, + } + } private _getMarketId = (): number => Number(this.id.split("-").slice(-1)[0]); @@ -807,8 +855,7 @@ export class LendMarketTemplate { } return tokens.map((token, i) => ({ token, symbol: tokenInfo[i * 2] as string, decimals: Number(tokenInfo[(i * 2) + 1]) })); - }, - { + }, { promise: true, maxAge: 30 * 60 * 1000, // 30m }); @@ -942,8 +989,7 @@ export class LendMarketTemplate { .map((_x) => formatUnits(_x * BigInt(100))); return { fee, admin_fee, liquidation_discount, loan_discount, base_price, A } - }, - { + }, { promise: true, maxAge: 5 * 60 * 1000, // 5m }); @@ -1404,7 +1450,7 @@ export class LendMarketTemplate { // ---------------- CREATE LOAN ---------------- - private _checkRange(range: number): void { + public _checkRange(range: number): void { if (range < this.minBands) throw Error(`range must be >= ${this.minBands}`); if (range > this.maxBands) throw Error(`range must be <= ${this.maxBands}`); } @@ -1460,7 +1506,7 @@ export class LendMarketTemplate { return await this.llamalend.multicallProvider.all(calls) as bigint[]; } - private async _getPrices(_n2: bigint, _n1: bigint): Promise { + public async _getPrices(_n2: bigint, _n1: bigint): Promise { const contract = this.llamalend.contracts[this.addresses.amm].multicallContract; return (await this.llamalend.multicallProvider.all([ contract.p_oracle_down(_n2), @@ -1468,7 +1514,7 @@ export class LendMarketTemplate { ]) as bigint[]).map((_p) => formatUnits(_p)); } - private async _calcPrices(_n2: bigint, _n1: bigint): Promise<[string, string]> { + public async _calcPrices(_n2: bigint, _n1: bigint): Promise<[string, string]> { return [await this.calcTickPrice(Number(_n2) + 1), await this.calcTickPrice(Number(_n1))]; } @@ -2078,13 +2124,13 @@ export class LendMarketTemplate { public async calcPartialFrac(amount: TAmount, address = ""): Promise { address = _getAddress.call(this.llamalend, address); const tokensToLiquidate = await this.tokensToLiquidate(address); - + const amountBN = BN(amount); const tokensToLiquidateBN = BN(tokensToLiquidate); - + if (amountBN.gt(tokensToLiquidateBN)) throw Error("Amount cannot be greater than total tokens to liquidate"); if (amountBN.lte(0)) throw Error("Amount must be greater than 0"); - + // Calculate frac = amount / tokensToLiquidate * 10**18 // 100% = 10**18 const fracDecimalBN = amountBN.div(tokensToLiquidateBN); @@ -2136,33 +2182,33 @@ export class LendMarketTemplate { if (slippage > 100) throw Error("Slippage must be <= 100"); if (Number(currentDebt) === 0) throw Error(`Loan for ${address} does not exist`); if (Number(borrowed) === 0) throw Error(`User ${address} is not in liquidation mode`); - + const frac = partialFrac.frac; const fracBN = BN(partialFrac.fracDecimal); - + const borrowedBN = BN(borrowed); const expectedBorrowedBN = borrowedBN.times(fracBN); const minAmountBN = expectedBorrowedBN.times(100 - slippage).div(100); const _minAmount = fromBN(minAmountBN); - + const contract = this.llamalend.contracts[this.addresses.controller].contract; const gas = (await contract.liquidate_extended.estimateGas( - address, - _minAmount, - frac, + address, + _minAmount, + frac, this.llamalend.constants.ZERO_ADDRESS, [], this.llamalend.constantOptions )); - + if (estimateGas) return smartNumber(gas); await this.llamalend.updateFeeData(); const gasLimit = _mulBy1_3(DIGas(gas)); return (await contract.liquidate_extended( - address, - _minAmount, - frac, + address, + _minAmount, + frac, this.llamalend.constants.ZERO_ADDRESS, [], { ...this.llamalend.options, gasLimit } @@ -2376,14 +2422,14 @@ export class LendMarketTemplate { const userEffectiveCollateralBN = BN(userCollateral).plus(BN(userBorrowed).div(pAvgBN as BigNumber)); const res: IDict<{ - maxDebt: string, - maxTotalCollateral: string, - userCollateral: string, - collateralFromUserBorrowed: string, - collateralFromMaxDebt: string, - maxLeverage: string, - avgPrice: string, - }> = {}; + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + maxLeverage: string, + avgPrice: string, + }> = {}; for (let N = this.minBands; N <= this.maxBands; N++) { const j = N - this.minBands; res[N] = { @@ -2783,7 +2829,7 @@ export class LendMarketTemplate { const _userBorrowed = parseUnits(userBorrowed, this.borrowed_token.decimals); await this._setSwapDataToCache(this.addresses.borrowed_token, this.addresses.collateral_token, _dDebt + _userBorrowed, slippage); const { _totalCollateral, _userCollateral, _collateralFromUserBorrowed, _collateralFromDebt, avgPrice } = - await this._leverageExpectedCollateral(userCollateral, userBorrowed, dDebt, address); + await this._leverageExpectedCollateral(userCollateral, userBorrowed, dDebt, address); return { totalCollateral: formatUnits(_totalCollateral, this.collateral_token.decimals), userCollateral: formatUnits(_userCollateral, this.collateral_token.decimals), @@ -3072,7 +3118,6 @@ export class LendMarketTemplate { calldata = await _assembleTxOdos.call(this.llamalend, swapData.pathId as string); } - console.log('params', [0, parseUnits(this._getMarketId(), 0), _userCollateral, _userBorrowed], calldata) const contract = this.llamalend.contracts[this.addresses.controller].contract; const gas = await contract.repay_extended.estimateGas( this.llamalend.constants.ALIASES.leverage_zap, @@ -3189,7 +3234,7 @@ export class LendMarketTemplate { public async forceUpdateUserState(newTx: string, userAddress?: string): Promise { const address = userAddress || this.llamalend.signerAddress; if (!address) throw Error("Need to connect wallet or pass address into args"); - + await _getUserCollateralForce( this.llamalend.constants.NETWORK_NAME, this.addresses.controller, @@ -3197,4 +3242,8 @@ export class LendMarketTemplate { newTx ); } -} + + public getLlamalend(): Llamalend { + return this.llamalend; + } +} \ No newline at end of file diff --git a/src/lendMarkets/interfaces/leverageZapV2.ts b/src/lendMarkets/interfaces/leverageZapV2.ts new file mode 100644 index 0000000..d5f1a88 --- /dev/null +++ b/src/lendMarkets/interfaces/leverageZapV2.ts @@ -0,0 +1,282 @@ +import {GetExpectedFn, IDict, ILeverageMetrics, IQuote, TAmount, TGas} from "../../interfaces.js"; + +export interface ILeverageZapV2 { + hasLeverage: () => boolean, + + maxLeverage: (N: number) => Promise, + + createLoanMaxRecv: ({ + userCollateral, + userBorrowed, + range, + getExpected, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + range: number, + getExpected: GetExpectedFn + }) => Promise<{ + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + maxLeverage: string, + avgPrice: string, + }>, + createLoanMaxRecvAllRanges: ({ + userCollateral, + userBorrowed, + getExpected, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + getExpected: GetExpectedFn + }) => Promise>, + createLoanExpectedCollateral: ({ + userCollateral, + userBorrowed, + debt, + quote, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + quote: IQuote + }) => Promise<{ + totalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromDebt: string, + leverage: string, + avgPrice: string + }>, + createLoanExpectedMetrics: ({ + userCollateral, + userBorrowed, + debt, + range, + quote, + healthIsFull, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + quote: IQuote, + healthIsFull?: boolean + }) => Promise, + createLoanMaxRange: ({ + userCollateral, + userBorrowed, + debt, + getExpected, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn + }) => Promise, + createLoanBandsAllRanges: ({ + userCollateral, + userBorrowed, + debt, + getExpected, + quote, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn, + quote: IQuote + }) => Promise>, + createLoanPricesAllRanges: ({ + userCollateral, + userBorrowed, + debt, + getExpected, + quote, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn, + quote: IQuote + }) => Promise>, + createLoanIsApproved: ({ + userCollateral, + userBorrowed, + }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + createLoanApprove: ({ + userCollateral, + userBorrowed, + }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + createLoan: ({ + userCollateral, + userBorrowed, + debt, + range, + router, + calldata, + }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + router: string, + calldata: string + }) => Promise, + + borrowMoreMaxRecv: ({ userCollateral, userBorrowed, getExpected, address }: { + userCollateral: TAmount, + userBorrowed: TAmount, + getExpected: GetExpectedFn, + address?: string + }) => Promise<{ + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + avgPrice: string, + }>, + borrowMoreExpectedCollateral: ({ userCollateral, userBorrowed, dDebt, quote, address }: { + userCollateral: TAmount, + userBorrowed: TAmount, + dDebt: TAmount, + quote: IQuote, + address?: string + }) => Promise<{ + totalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromDebt: string, + avgPrice: string + }>, + borrowMoreExpectedMetrics: ({ userCollateral, userBorrowed, debt, quote, healthIsFull, address }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + quote: IQuote, + healthIsFull?: boolean, + address?: string + }) => Promise, + borrowMoreIsApproved: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + borrowMoreApprove: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + borrowMore: ({ userCollateral, userBorrowed, debt, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + router: string, + calldata: string + }) => Promise, + + repayExpectedBorrowed: ({ stateCollateral, userCollateral, userBorrowed, quote }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote + }) => Promise<{ + totalBorrowed: string, + borrowedFromStateCollateral: string, + borrowedFromUserCollateral: string, + userBorrowed: string, + avgPrice: string + }>, + repayIsFull: ({ stateCollateral, userCollateral, userBorrowed, quote, address }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote, + address?: string + }) => Promise, + repayIsAvailable: ({ stateCollateral, userCollateral, userBorrowed, quote, address }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote, + address?: string + }) => Promise, + repayExpectedMetrics: ({ stateCollateral, userCollateral, userBorrowed, healthIsFull, quote, address }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + healthIsFull: boolean, + quote: IQuote, + address: string + }) => Promise, + repayIsApproved: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + repayApprove: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + repay: ({ stateCollateral, userCollateral, userBorrowed, router, calldata }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + router: string, + calldata: string + }) => Promise, + + estimateGas: { + createLoanApprove: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + createLoan: ({ userCollateral, userBorrowed, debt, range, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + router: string, + calldata: string + }) => Promise, + + borrowMoreApprove: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + borrowMore: ({ userCollateral, userBorrowed, debt, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + router: string, + calldata: string + }) => Promise, + + repayApprove: ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }) => Promise, + repay: ({ stateCollateral, userCollateral, userBorrowed, router, calldata }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + router: string, + calldata: string + }) => Promise, + } +} diff --git a/src/lendMarkets/modules/index.ts b/src/lendMarkets/modules/index.ts new file mode 100644 index 0000000..496aeea --- /dev/null +++ b/src/lendMarkets/modules/index.ts @@ -0,0 +1 @@ +export { LeverageZapV2Module } from './leverageZapV2.js'; \ No newline at end of file diff --git a/src/lendMarkets/modules/leverageZapV2.ts b/src/lendMarkets/modules/leverageZapV2.ts new file mode 100644 index 0000000..e6cac4e --- /dev/null +++ b/src/lendMarkets/modules/leverageZapV2.ts @@ -0,0 +1,991 @@ +import memoize from "memoizee"; +import type { TAmount, TGas, IDict, IQuote, ILeverageMetrics, GetExpectedFn } from "../../interfaces.js"; +import type { LendMarketTemplate } from "../LendMarketTemplate.js"; +import { + _getAddress, + parseUnits, + BN, + toBN, + fromBN, + ensureAllowance, + hasAllowance, + ensureAllowanceEstimateGas, + formatUnits, + smartNumber, + formatNumber, + _mulBy1_3, + DIGas, + buildCalldataForLeverageZapV2, +} from "../../utils.js"; +import {Llamalend} from "../../llamalend.js"; +import BigNumber from "bignumber.js"; + +/** + * LeverageZapV2 module for LendMarketTemplate + */ +export class LeverageZapV2Module { + private market: LendMarketTemplate; + private llamalend: Llamalend; + + constructor(market: LendMarketTemplate) { + this.market = market; + this.llamalend = market.getLlamalend(); + } + + private _getMarketId = (): number => Number(this.market.id.split("-").slice(-1)[0]); + + // ============ CREATE LOAN METHODS ============ + + public hasLeverage = (): boolean => { + return this.llamalend.constants.ALIASES.leverage_zap_v2 !== this.llamalend.constants.ZERO_ADDRESS && + this._getMarketId() >= Number(this.llamalend.constants.ALIASES["leverage_markets_start_id"]); + } + + public _checkLeverageZap(): void { + if (!this.hasLeverage()) { + throw Error("This market does not support leverage"); + } + } + + public async _get_k_effective_BN(N: number): Promise { + // d_k_effective: uint256 = (1 - loan_discount) * sqrt((A-1)/A) / N + // k_effective = d_k_effective * sum_{0..N-1}(((A-1) / A)**k) + const { loan_discount, A } = await this.market.stats.parameters(); + const A_BN = BN(A); + const A_ratio_BN = A_BN.minus(1).div(A_BN); + + const d_k_effective_BN = BN(100).minus(loan_discount).div(100).times(A_ratio_BN.sqrt()).div(N); + let S = BN(0); + for (let n = 0; n < N; n++) { + S = S.plus(A_ratio_BN.pow(n)) + } + + return d_k_effective_BN.times(S); + } + + public async maxLeverage(N: number): Promise { + // max_leverage = 1 / (k_effective - 1) + const k_effective_BN = await this._get_k_effective_BN(N); + + return BN(1).div(BN(1).minus(k_effective_BN)).toString() + } + + public async leverageCreateLoanMaxRecv({ userCollateral, userBorrowed, range, getExpected }: { + userCollateral: TAmount, + userBorrowed: TAmount, + range: number, + getExpected: GetExpectedFn + }): Promise<{ + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + maxLeverage: string, + avgPrice: string, + }> { + // max_borrowable = userCollateral / (1 / (k_effective * max_p_base) - 1 / p_avg) + this._checkLeverageZap(); + if (range > 0) this.market._checkRange(range); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const _userBorrowed = parseUnits(userBorrowed, this.market.borrowed_token.decimals); + + const oraclePriceBand = await this.market.oraclePriceBand(); + let pAvgBN = BN(await this.market.calcTickPrice(oraclePriceBand)); // upper tick of oracle price band + let maxBorrowablePrevBN = BN(0); + let maxBorrowableBN = BN(0); + let _userEffectiveCollateral = BigInt(0); + let _maxLeverageCollateral = BigInt(0); + + const contract = this.llamalend.contracts[this.llamalend.constants.ALIASES.leverage_zap_v2].contract; + for (let i = 0; i < 5; i++) { + maxBorrowablePrevBN = maxBorrowableBN; + _userEffectiveCollateral = _userCollateral + fromBN(BN(userBorrowed).div(pAvgBN), this.market.collateral_token.decimals); + let _maxBorrowable = await contract.max_borrowable(this.market.addresses.controller, _userEffectiveCollateral, _maxLeverageCollateral, range, fromBN(pAvgBN)); + _maxBorrowable = _maxBorrowable * BigInt(998) / BigInt(1000) + if (_maxBorrowable === BigInt(0)) break; + maxBorrowableBN = toBN(_maxBorrowable, this.market.borrowed_token.decimals); + + if (maxBorrowableBN.minus(maxBorrowablePrevBN).abs().div(maxBorrowablePrevBN).lt(0.0005)) { + maxBorrowableBN = maxBorrowablePrevBN; + break; + } + + // additionalCollateral = (userBorrowed / p) + leverageCollateral + const _maxAdditionalCollateral = BigInt((await getExpected( + this.market.addresses.borrowed_token, + this.market.addresses.collateral_token, + _maxBorrowable + _userBorrowed, + this.market.addresses.amm + )).outAmount); + + pAvgBN = maxBorrowableBN.plus(userBorrowed).div(toBN(_maxAdditionalCollateral, this.market.collateral_token.decimals)); + _maxLeverageCollateral = _maxAdditionalCollateral - fromBN(BN(userBorrowed).div(pAvgBN), this.market.collateral_token.decimals); + } + + const userEffectiveCollateralBN = maxBorrowableBN.gt(0) ? toBN(_userEffectiveCollateral, this.market.collateral_token.decimals) : BN(0); + const maxLeverageCollateralBN = toBN(_maxLeverageCollateral, this.market.collateral_token.decimals); + + return { + maxDebt: formatNumber(maxBorrowableBN.toString(), this.market.borrowed_token.decimals), + maxTotalCollateral: formatNumber(maxLeverageCollateralBN.plus(userEffectiveCollateralBN).toString(), this.market.collateral_token.decimals), + userCollateral: formatNumber(userCollateral, this.market.collateral_token.decimals), + collateralFromUserBorrowed: formatNumber(BN(userBorrowed).div(pAvgBN).toString(), this.market.collateral_token.decimals), + collateralFromMaxDebt: formatNumber(maxLeverageCollateralBN.toString(), this.market.collateral_token.decimals), + maxLeverage: maxLeverageCollateralBN.plus(userEffectiveCollateralBN).div(userEffectiveCollateralBN).toString(), + avgPrice: pAvgBN.toString(), + }; + } + + public leverageCreateLoanMaxRecvAllRanges = memoize(async ({ userCollateral, userBorrowed, getExpected }: { + userCollateral: TAmount, + userBorrowed: TAmount, + getExpected: GetExpectedFn + }): Promise> => { + this._checkLeverageZap(); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const contract = this.llamalend.contracts[this.llamalend.constants.ALIASES.leverage_zap_v2].multicallContract; + + const oraclePriceBand = await this.market.oraclePriceBand(); + const pAvgApproxBN = BN(await this.market.calcTickPrice(oraclePriceBand)); // upper tick of oracle price band + let pAvgBN: BigNumber | null = null; + const arrLength = this.market.maxBands - this.market.minBands + 1; + let maxLeverageCollateralBN: BigNumber[] = new Array(arrLength).fill(BN(0)); + let _maxLeverageCollateral: bigint[] = new Array(arrLength).fill(BigInt(0)); + let maxBorrowablePrevBN: BigNumber[] = new Array(arrLength).fill(BN(0)); + let maxBorrowableBN: BigNumber[] = new Array(arrLength).fill(BN(0)); + let _maxBorrowable: bigint[] = new Array(arrLength).fill(BigInt(0)); + + for (let i = 0; i < 5; i++) { + const pBN = pAvgBN ?? pAvgApproxBN; + maxBorrowablePrevBN = maxBorrowableBN; + const _userEffectiveCollateral: bigint = _userCollateral + fromBN(BN(userBorrowed).div(pBN), this.market.collateral_token.decimals); + const calls = []; + for (let N = this.market.minBands; N <= this.market.maxBands; N++) { + const j = N - this.market.minBands; + calls.push(contract.max_borrowable(this.market.addresses.controller, _userEffectiveCollateral, _maxLeverageCollateral[j], N, fromBN(pBN))); + } + _maxBorrowable = (await this.llamalend.multicallProvider.all(calls) as bigint[]).map((_mb) => _mb * BigInt(998) / BigInt(1000)); + maxBorrowableBN = _maxBorrowable.map((_mb) => toBN(_mb, this.market.borrowed_token.decimals)); + + const deltaBN = maxBorrowableBN.map((mb, l) => mb.minus(maxBorrowablePrevBN[l]).abs().div(mb)); + if (BigNumber.max(...deltaBN).lt(0.0005)) { + maxBorrowableBN = maxBorrowablePrevBN; + break; + } + + if (pAvgBN === null){ + const _y = BigInt((await getExpected( + this.market.addresses.borrowed_token, + this.market.addresses.collateral_token, + _maxBorrowable[0], + this.market.addresses.amm + )).outAmount); + const yBN = toBN(_y, this.market.collateral_token.decimals); + pAvgBN = maxBorrowableBN[0].div(yBN); + } + + maxLeverageCollateralBN = maxBorrowableBN.map((mb) => mb.div(pAvgBN as BigNumber)); + _maxLeverageCollateral = maxLeverageCollateralBN.map((mlc) => fromBN(mlc, this.market.collateral_token.decimals)); + } + + const userEffectiveCollateralBN = BN(userCollateral).plus(BN(userBorrowed).div(pAvgBN as BigNumber)); + + const res: IDict<{ + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + maxLeverage: string, + avgPrice: string, + }> = {}; + for (let N = this.market.minBands; N <= this.market.maxBands; N++) { + const j = N - this.market.minBands; + res[N] = { + maxDebt: formatNumber(maxBorrowableBN[j].toString(), this.market.borrowed_token.decimals), + maxTotalCollateral: formatNumber(maxLeverageCollateralBN[j].plus(userEffectiveCollateralBN).toString(), this.market.collateral_token.decimals), + userCollateral: formatNumber(userCollateral, this.market.collateral_token.decimals), + collateralFromUserBorrowed: formatNumber(BN(userBorrowed).div(pAvgBN as BigNumber).toString(), this.market.collateral_token.decimals), + collateralFromMaxDebt: formatNumber(maxLeverageCollateralBN[j].toString(), this.market.collateral_token.decimals), + maxLeverage: maxLeverageCollateralBN[j].plus(userEffectiveCollateralBN).div(userEffectiveCollateralBN).toString(), + avgPrice: (pAvgBN as BigNumber).toString(), + }; + } + + return res; + }, + { + promise: true, + maxAge: 60 * 1000, // 1m + }); + + private _leverageExpectedCollateral = async (userCollateral: TAmount, userBorrowed: TAmount, debt: TAmount, quote: IQuote, user?: string): + Promise<{ _futureStateCollateral: bigint, _totalCollateral: bigint, _userCollateral: bigint, + _collateralFromUserBorrowed: bigint, _collateralFromDebt: bigint, avgPrice: string }> => { + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const _debt = parseUnits(debt, this.market.borrowed_token.decimals); + const _userBorrowed = parseUnits(userBorrowed, this.market.borrowed_token.decimals); + // additionalCollateral = (userBorrowed / p) + leverageCollateral + const _additionalCollateral = BigInt(quote.outAmount); + const _collateralFromDebt = _debt * BigInt(10**18) / (_debt + _userBorrowed) * _additionalCollateral / BigInt(10**18); + const _collateralFromUserBorrowed = _additionalCollateral - _collateralFromDebt; + let _stateCollateral = BigInt(0); + if (user) { + const { _collateral, _borrowed } = await this.market._userState(user); + if (_borrowed > BigInt(0)) throw Error(`User ${user} is already in liquidation mode`); + _stateCollateral = _collateral; + } + const _totalCollateral = _userCollateral + _additionalCollateral; + const _futureStateCollateral = _stateCollateral + _totalCollateral; + const avgPrice = toBN(_debt + _userBorrowed, this.market.borrowed_token.decimals).div(toBN(_additionalCollateral, this.market.collateral_token.decimals)).toString(); + + return { _futureStateCollateral, _totalCollateral, _userCollateral, _collateralFromUserBorrowed, _collateralFromDebt, avgPrice }; + }; + + public async leverageCreateLoanExpectedCollateral({ userCollateral, userBorrowed, debt, quote }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + quote: IQuote + }): Promise<{ totalCollateral: string, userCollateral: string, collateralFromUserBorrowed: string, collateralFromDebt: string, leverage: string, avgPrice: string }> { + this._checkLeverageZap(); + + const { _totalCollateral, _userCollateral, _collateralFromUserBorrowed, _collateralFromDebt, avgPrice } = + await this._leverageExpectedCollateral(userCollateral, userBorrowed, debt, quote); + return { + totalCollateral: formatUnits(_totalCollateral, this.market.collateral_token.decimals), + userCollateral: formatUnits(_userCollateral, this.market.collateral_token.decimals), + collateralFromUserBorrowed: formatUnits(_collateralFromUserBorrowed, this.market.collateral_token.decimals), + collateralFromDebt: formatUnits(_collateralFromDebt, this.market.collateral_token.decimals), + leverage: toBN(_collateralFromDebt + _userCollateral + _collateralFromUserBorrowed, this.market.collateral_token.decimals) + .div(toBN(_userCollateral + _collateralFromUserBorrowed, this.market.collateral_token.decimals)).toString(), + avgPrice, + } + } + + public async leverageCreateLoanExpectedMetrics({ userCollateral, userBorrowed, debt, range, quote, healthIsFull = true }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + quote: IQuote, + healthIsFull?: boolean + }): Promise { + this._checkLeverageZap(); + + const [_n2, _n1] = await this._leverageBands(userCollateral, userBorrowed, debt, range, quote); + + const prices = await this.market._getPrices(_n2, _n1); + const health = await this._leverageHealth(userCollateral, userBorrowed, debt, range, quote, healthIsFull); + + return { + priceImpact: quote.priceImpact, + bands: [Number(_n2), Number(_n1)], + prices, + health, + } + } + + public async leverageCreateLoanMaxRange({ userCollateral, userBorrowed, debt, getExpected }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn + }): Promise { + this._checkLeverageZap(); + const maxRecv = await this.leverageCreateLoanMaxRecvAllRanges({ userCollateral, userBorrowed, getExpected }); + for (let N = this.market.minBands; N <= this.market.maxBands; N++) { + if (BN(debt).gt(maxRecv[N].maxDebt)) return N - 1; + } + + return this.market.maxBands; + } + + private _leverageCalcN1 = memoize(async (userCollateral: TAmount, userBorrowed: TAmount, debt: TAmount, range: number, quote: IQuote, user?: string): Promise => { + if (range > 0) this.market._checkRange(range); + let _stateDebt = BigInt(0); + if (user) { + const { _debt, _borrowed, _N } = await this.market._userState(user); + if (_borrowed > BigInt(0)) throw Error(`User ${user} is already in liquidation mode`); + _stateDebt = _debt; + if (range < 0) range = Number(this.llamalend.formatUnits(_N, 0)); + } + const { _futureStateCollateral } = await this._leverageExpectedCollateral(userCollateral, userBorrowed, debt, quote, user); + const _debt = _stateDebt + parseUnits(debt, this.market.borrowed_token.decimals); + return await this.llamalend.contracts[this.market.addresses.controller].contract.calculate_debt_n1(_futureStateCollateral, _debt, range, this.llamalend.constantOptions); + }, + { + promise: true, + maxAge: 60 * 1000, // 1m + }); + + private _leverageCalcN1AllRanges = memoize(async (userCollateral: TAmount, userBorrowed: TAmount, debt: TAmount, maxN: number, quote: IQuote): Promise => { + const { _futureStateCollateral } = await this._leverageExpectedCollateral(userCollateral, userBorrowed, debt, quote); + const _debt = parseUnits(debt, this.market.borrowed_token.decimals); + const calls = []; + for (let N = this.market.minBands; N <= maxN; N++) { + calls.push(this.llamalend.contracts[this.market.addresses.controller].multicallContract.calculate_debt_n1(_futureStateCollateral, _debt, N)); + } + return await this.llamalend.multicallProvider.all(calls) as bigint[]; + }, + { + promise: true, + maxAge: 60 * 1000, // 1m + }); + + private async _leverageBands(userCollateral: TAmount, userBorrowed: TAmount, debt: TAmount, range: number, quote: IQuote, user?: string): Promise<[bigint, bigint]> { + const _n1 = await this._leverageCalcN1(userCollateral, userBorrowed, debt, range, quote, user); + if (range < 0) { + const { N } = await this.market.userState(user); + range = Number(N); + } + const _n2 = _n1 + BigInt(range - 1); + + return [_n2, _n1]; + } + + private async _leverageCreateLoanBandsAllRanges(userCollateral: TAmount, userBorrowed: TAmount, debt: TAmount, getExpected: GetExpectedFn, quote: IQuote): Promise> { + const maxN = await this.leverageCreateLoanMaxRange({ userCollateral, userBorrowed, debt, getExpected }); + const _n1_arr = await this._leverageCalcN1AllRanges(userCollateral, userBorrowed, debt, maxN, quote); + const _n2_arr: bigint[] = []; + for (let N = this.market.minBands; N <= maxN; N++) { + _n2_arr.push(_n1_arr[N - this.market.minBands] + BigInt(N - 1)); + } + + const _bands: IDict<[bigint, bigint]> = {}; + for (let N = this.market.minBands; N <= maxN; N++) { + _bands[N] = [_n2_arr[N - this.market.minBands], _n1_arr[N - this.market.minBands]]; + } + + return _bands; + } + + public async leverageCreateLoanBandsAllRanges({ userCollateral, userBorrowed, debt, getExpected, quote }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn, + quote: IQuote + }): Promise> { + this._checkLeverageZap(); + const _bands = await this._leverageCreateLoanBandsAllRanges(userCollateral, userBorrowed, debt, getExpected, quote); + + const bands: { [index: number]: [number, number] | null } = {}; + for (let N = this.market.minBands; N <= this.market.maxBands; N++) { + if (_bands[N]) { + bands[N] = _bands[N].map(Number) as [number, number]; + } else { + bands[N] = null + } + } + + return bands; + } + + public async leverageCreateLoanPricesAllRanges({ userCollateral, userBorrowed, debt, getExpected, quote }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + getExpected: GetExpectedFn, + quote: IQuote + }): Promise> { + this._checkLeverageZap(); + const _bands = await this._leverageCreateLoanBandsAllRanges(userCollateral, userBorrowed, debt, getExpected, quote); + + const prices: { [index: number]: [string, string] | null } = {}; + for (let N = this.market.minBands; N <= this.market.maxBands; N++) { + if (_bands[N]) { + prices[N] = await this.market._calcPrices(..._bands[N]); + } else { + prices[N] = null + } + } + + return prices; + } + + private async _leverageHealth( + userCollateral: TAmount, + userBorrowed: TAmount, + dDebt: TAmount, + range: number, + quote: IQuote, + full: boolean, + user = this.llamalend.constants.ZERO_ADDRESS + ): Promise { + if (range > 0) this.market._checkRange(range); + const { _totalCollateral } = await this._leverageExpectedCollateral(userCollateral, userBorrowed, dDebt, quote, user); + const { _borrowed, _N } = await this.market._userState(user); + if (_borrowed > BigInt(0)) throw Error(`User ${user} is already in liquidation mode`); + if (range < 0) range = Number(this.llamalend.formatUnits(_N, 0)); + const _dDebt = parseUnits(dDebt, this.market.borrowed_token.decimals); + + const contract = this.llamalend.contracts[this.market.addresses.controller].contract; + let _health = await contract.health_calculator(user, _totalCollateral, _dDebt, full, range, this.llamalend.constantOptions) as bigint; + _health = _health * BigInt(100); + + return formatUnits(_health); + } + + public async leverageCreateLoanIsApproved({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + const collateralAllowance = await hasAllowance.call(this.llamalend, + [this.market.collateral_token.address], [userCollateral], this.llamalend.signerAddress, this.market.addresses.controller); + const borrowedAllowance = await hasAllowance.call(this.llamalend, + [this.market.borrowed_token.address], [userBorrowed], this.llamalend.signerAddress, this.llamalend.constants.ALIASES.leverage_zap_v2); + + return collateralAllowance && borrowedAllowance + } + + public async leverageCreateLoanApproveEstimateGas ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + const collateralGas = await ensureAllowanceEstimateGas.call(this.llamalend, + [this.market.collateral_token.address], [userCollateral], this.market.addresses.controller); + const borrowedGas = await ensureAllowanceEstimateGas.call(this.llamalend, + [this.market.borrowed_token.address], [userBorrowed], this.llamalend.constants.ALIASES.leverage_zap_v2); + + if(Array.isArray(collateralGas) && Array.isArray(borrowedGas)) { + return [collateralGas[0] + borrowedGas[0], collateralGas[1] + borrowedGas[1]] + } else { + return (collateralGas as number) + (borrowedGas as number) + } + } + + public async leverageCreateLoanApprove({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + const collateralApproveTx = await ensureAllowance.call(this.llamalend, + [this.market.collateral_token.address], [userCollateral], this.market.addresses.controller); + const borrowedApproveTx = await ensureAllowance.call(this.llamalend, + [this.market.borrowed_token.address], [userBorrowed], this.llamalend.constants.ALIASES.leverage_zap_v2); + + return [...collateralApproveTx, ...borrowedApproveTx] + } + + private async _leverageCreateLoan( + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + router: string, + calldata: string, + estimateGas: boolean + ): Promise { + if (await this.market.userLoanExists()) throw Error("Loan already created"); + this.market._checkRange(range); + + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const _userBorrowed = parseUnits(userBorrowed, this.market.borrowed_token.decimals); + const _debt = parseUnits(debt, this.market.borrowed_token.decimals); + + + const zapCalldata = buildCalldataForLeverageZapV2(router, calldata) + const contract = this.llamalend.contracts[this.market.addresses.controller].contract; + const gas = await contract.create_loan_extended.estimateGas( + _userCollateral, + _debt, + range, + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userBorrowed], + zapCalldata, + { ...this.llamalend.constantOptions } + ); + if (estimateGas) return smartNumber(gas); + + await this.llamalend.updateFeeData(); + const gasLimit = _mulBy1_3(DIGas(gas)); + return (await contract.create_loan_extended( + _userCollateral, + _debt, + range, + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userBorrowed], + zapCalldata, + { ...this.llamalend.options, gasLimit } + )).hash + } + + public async leverageCreateLoanEstimateGas({ userCollateral, userBorrowed, debt, range, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + if (!(await this.leverageCreateLoanIsApproved({ userCollateral, userBorrowed }))) throw Error("Approval is needed for gas estimation"); + return await this._leverageCreateLoan(userCollateral, userBorrowed, debt, range, router, calldata, true) as number; + } + + public async leverageCreateLoan({ userCollateral, userBorrowed, debt, range, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + range: number, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + await this.leverageCreateLoanApprove({ userCollateral, userBorrowed }); + return await this._leverageCreateLoan(userCollateral, userBorrowed, debt, range, router, calldata, false) as string; + } + + // ---------------- LEVERAGE BORROW MORE ---------------- + + public async leverageBorrowMoreMaxRecv({ userCollateral, userBorrowed, getExpected, address = "" }: { + userCollateral: TAmount, + userBorrowed: TAmount, + getExpected: GetExpectedFn, + address?: string + }): Promise<{ + maxDebt: string, + maxTotalCollateral: string, + userCollateral: string, + collateralFromUserBorrowed: string, + collateralFromMaxDebt: string, + avgPrice: string, + }> { + // max_borrowable = userCollateral / (1 / (k_effective * max_p_base) - 1 / p_avg) + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + const { _collateral: _stateCollateral, _borrowed: _stateBorrowed, _debt: _stateDebt, _N } = await this.market._userState(address); + if (_stateBorrowed > BigInt(0)) throw Error(`User ${address} is already in liquidation mode`); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const controllerContract = this.llamalend.contracts[this.market.addresses.controller].contract; + const _borrowedFromStateCollateral = await controllerContract.max_borrowable(_stateCollateral, _N, _stateDebt, this.llamalend.constantOptions) - _stateDebt; + const _userBorrowed = _borrowedFromStateCollateral + parseUnits(userBorrowed, this.market.borrowed_token.decimals); + userBorrowed = formatUnits(_userBorrowed, this.market.borrowed_token.decimals); + + const oraclePriceBand = await this.market.oraclePriceBand(); + let pAvgBN = BN(await this.market.calcTickPrice(oraclePriceBand)); // upper tick of oracle price band + let maxBorrowablePrevBN = BN(0); + let maxBorrowableBN = BN(0); + let _userEffectiveCollateral = BigInt(0); + let _maxLeverageCollateral = BigInt(0); + + const contract = this.llamalend.contracts[this.llamalend.constants.ALIASES.leverage_zap_v2].contract; + for (let i = 0; i < 5; i++) { + maxBorrowablePrevBN = maxBorrowableBN; + _userEffectiveCollateral = _userCollateral + fromBN(BN(userBorrowed).div(pAvgBN), this.market.collateral_token.decimals); + let _maxBorrowable = await contract.max_borrowable(this.market.addresses.controller, _userEffectiveCollateral, _maxLeverageCollateral, _N, fromBN(pAvgBN)); + _maxBorrowable = _maxBorrowable * BigInt(998) / BigInt(1000); + if (_maxBorrowable === BigInt(0)) break; + maxBorrowableBN = toBN(_maxBorrowable, this.market.borrowed_token.decimals); + + if (maxBorrowableBN.minus(maxBorrowablePrevBN).abs().div(maxBorrowablePrevBN).lt(0.0005)) { + maxBorrowableBN = maxBorrowablePrevBN; + break; + } + + // additionalCollateral = (userBorrowed / p) + leverageCollateral + const _maxAdditionalCollateral = BigInt((await getExpected( + this.market.addresses.borrowed_token, this.market.addresses.collateral_token, _maxBorrowable + _userBorrowed, this.market.addresses.amm)).outAmount); + pAvgBN = maxBorrowableBN.plus(userBorrowed).div(toBN(_maxAdditionalCollateral, this.market.collateral_token.decimals)); + _maxLeverageCollateral = _maxAdditionalCollateral - fromBN(BN(userBorrowed).div(pAvgBN), this.market.collateral_token.decimals); + } + + if (maxBorrowableBN.eq(0)) _userEffectiveCollateral = BigInt(0); + const _maxTotalCollateral = _userEffectiveCollateral + _maxLeverageCollateral + let _maxBorrowable = await controllerContract.max_borrowable(_stateCollateral + _maxTotalCollateral, _N, _stateDebt, this.llamalend.constantOptions) - _stateDebt; + _maxBorrowable = _maxBorrowable * BigInt(998) / BigInt(1000); + + return { + maxDebt: formatUnits(_maxBorrowable, this.market.borrowed_token.decimals), + maxTotalCollateral: formatUnits(_maxTotalCollateral, this.market.collateral_token.decimals), + userCollateral: formatNumber(userCollateral, this.market.collateral_token.decimals), + collateralFromUserBorrowed: formatNumber(BN(userBorrowed).div(pAvgBN).toString(), this.market.collateral_token.decimals), + collateralFromMaxDebt: formatUnits(_maxLeverageCollateral, this.market.collateral_token.decimals), + avgPrice: pAvgBN.toString(), + }; + } + + public async leverageBorrowMoreExpectedCollateral({ userCollateral, userBorrowed, dDebt, quote, address = "" }: { + userCollateral: TAmount, + userBorrowed: TAmount, + dDebt: TAmount, + quote: IQuote, + address?: string + }): Promise<{ totalCollateral: string, userCollateral: string, collateralFromUserBorrowed: string, collateralFromDebt: string, avgPrice: string }> { + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + + const { _totalCollateral, _userCollateral, _collateralFromUserBorrowed, _collateralFromDebt, avgPrice } = + await this._leverageExpectedCollateral(userCollateral, userBorrowed, dDebt, quote, address); + return { + totalCollateral: formatUnits(_totalCollateral, this.market.collateral_token.decimals), + userCollateral: formatUnits(_userCollateral, this.market.collateral_token.decimals), + collateralFromUserBorrowed: formatUnits(_collateralFromUserBorrowed, this.market.collateral_token.decimals), + collateralFromDebt: formatUnits(_collateralFromDebt, this.market.collateral_token.decimals), + avgPrice, + } + } + + public async leverageBorrowMoreExpectedMetrics({ userCollateral, userBorrowed, debt, quote, healthIsFull = true, address = "" }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + quote: IQuote, + healthIsFull?: boolean, + address?: string + }): Promise { + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + + + const [_n2, _n1] = await this._leverageBands(userCollateral, userBorrowed, debt, -1, quote, address); + + const prices = await this.market._getPrices(_n2, _n1); + const health = await this._leverageHealth(userCollateral, userBorrowed, debt, -1, quote, healthIsFull, address); + + return { + priceImpact: quote.priceImpact, + bands: [Number(_n2), Number(_n1)], + prices, + health, + } + } + + private async _leverageBorrowMore( + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + router: string, + calldata: string, + estimateGas: boolean + ): Promise { + if (!(await this.market.userLoanExists())) throw Error("Loan does not exist"); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const _userBorrowed = parseUnits(userBorrowed, this.market.borrowed_token.decimals); + const _debt = parseUnits(debt, this.market.borrowed_token.decimals); + + const zapCalldata = buildCalldataForLeverageZapV2(router, calldata); + + const contract = this.llamalend.contracts[this.market.addresses.controller].contract; + const gas = await contract.borrow_more_extended.estimateGas( + _userCollateral, + _debt, + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userBorrowed], + zapCalldata, + { ...this.llamalend.constantOptions } + ); + if (estimateGas) return smartNumber(gas); + + await this.llamalend.updateFeeData(); + const gasLimit = _mulBy1_3(DIGas(gas)); + + return (await contract.borrow_more_extended( + _userCollateral, + _debt, + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userBorrowed], + zapCalldata, + { ...this.llamalend.options, gasLimit } + )).hash + } + + public async leverageBorrowMoreIsApproved({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + return await this.leverageCreateLoanIsApproved({ userCollateral, userBorrowed }); + } + + public async leverageBorrowMoreApprove({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + return await this.leverageCreateLoanApprove({ userCollateral, userBorrowed }); + } + + public async leverageBorrowMoreEstimateGas({ userCollateral, userBorrowed, debt, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + if (!(await this.leverageCreateLoanIsApproved({ userCollateral, userBorrowed }))) throw Error("Approval is needed for gas estimation"); + return await this._leverageBorrowMore(userCollateral, userBorrowed, debt, router, calldata, true) as number; + } + + public async leverageBorrowMore({ userCollateral, userBorrowed, debt, router, calldata }: { + userCollateral: TAmount, + userBorrowed: TAmount, + debt: TAmount, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + await this.leverageCreateLoanApprove({ userCollateral, userBorrowed }); + return await this._leverageBorrowMore(userCollateral, userBorrowed, debt, router, calldata, false) as string; + } + + // ---------------- LEVERAGE REPAY ---------------- + + private _leverageRepayExpectedBorrowed = (stateCollateral: TAmount, userCollateral: TAmount, userBorrowed: TAmount, quote: IQuote): + { _totalBorrowed: bigint, _borrowedFromStateCollateral: bigint, _borrowedFromUserCollateral: bigint, avgPrice: string } => { + this._checkLeverageZap(); + const _stateCollateral = parseUnits(stateCollateral, this.market.collateral_token.decimals); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + let _borrowedExpected = BigInt(0); + let _borrowedFromStateCollateral = BigInt(0); + let _borrowedFromUserCollateral = BigInt(0); + if (_stateCollateral + _userCollateral > BigInt(0)) { + _borrowedExpected = BigInt(quote.outAmount); + _borrowedFromStateCollateral = _stateCollateral * BigInt(10 ** 18) / (_stateCollateral + _userCollateral) * _borrowedExpected / BigInt(10 ** 18); + _borrowedFromUserCollateral = _borrowedExpected - _borrowedFromStateCollateral; + } + const _totalBorrowed = _borrowedExpected + parseUnits(userBorrowed, this.market.borrowed_token.decimals); + const avgPrice = toBN(_borrowedExpected, this.market.borrowed_token.decimals).div(toBN(_stateCollateral + _userCollateral, this.market.collateral_token.decimals)).toString(); + + return { _totalBorrowed, _borrowedFromStateCollateral, _borrowedFromUserCollateral, avgPrice } + }; + + public leverageRepayExpectedBorrowed = async ({ stateCollateral, userCollateral, userBorrowed, quote }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote + }): Promise<{ totalBorrowed: string, borrowedFromStateCollateral: string, borrowedFromUserCollateral: string, userBorrowed: string, avgPrice: string }> => { + this._checkLeverageZap(); + + const { _totalBorrowed, _borrowedFromStateCollateral, _borrowedFromUserCollateral, avgPrice } = + this._leverageRepayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed, quote); + + return { + totalBorrowed: formatUnits(_totalBorrowed, this.market.borrowed_token.decimals), + borrowedFromStateCollateral: formatUnits(_borrowedFromStateCollateral, this.market.borrowed_token.decimals), + borrowedFromUserCollateral: formatUnits(_borrowedFromUserCollateral, this.market.borrowed_token.decimals), + userBorrowed: formatNumber(userBorrowed, this.market.borrowed_token.decimals), + avgPrice, + } + }; + + public async leverageRepayIsFull({ stateCollateral, userCollateral, userBorrowed, quote, address = "" }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote, + address?: string + }): Promise { + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + const { _borrowed: _stateBorrowed, _debt } = await this.market._userState(address); + const { _totalBorrowed } = this._leverageRepayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed, quote); + + return _stateBorrowed + _totalBorrowed > _debt; + } + + public async leverageRepayIsAvailable({ stateCollateral, userCollateral, userBorrowed, quote, address = "" }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + quote: IQuote, + address?: string + }): Promise { + // 0. const { collateral, stablecoin, debt } = await this.market.userState(address); + // 1. maxCollateral for deleverage is collateral from line above. + // 2. If user is underwater (stablecoin > 0), only full repayment is available: + // await this.deleverageRepayStablecoins(deleverageCollateral) + stablecoin > debt + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + const { collateral, borrowed, debt } = await this.market.userState(address); + // Loan does not exist + if (BN(debt).eq(0)) return false; + // Can't spend more than user has + if (BN(stateCollateral).gt(collateral)) return false; + // Only full repayment and closing the position is available if user is underwater+ + if (BN(borrowed).gt(0)) return await this.leverageRepayIsFull({ stateCollateral, userCollateral, userBorrowed, quote, address }); + + return true; + } + + public async leverageRepayExpectedMetrics({ stateCollateral, userCollateral, userBorrowed, healthIsFull, quote, address }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + healthIsFull: boolean, + quote: IQuote, + address: string + }): Promise { + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + + const [_n2, _n1] = await this._leverageRepayBands(stateCollateral, userCollateral, userBorrowed, quote, address); + const prices = await this.market._getPrices(_n2, _n1); + const health = await this._leverageRepayHealth(stateCollateral, userCollateral, userBorrowed, quote, healthIsFull, address); + + const _stateCollateral = parseUnits(stateCollateral, this.market.collateral_token.decimals); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const priceImpact = _stateCollateral + _userCollateral > BigInt(0) ? quote.priceImpact : 0; + + return { + priceImpact, + bands: [Number(_n2), Number(_n1)], + prices, + health, + } + } + + private _leverageRepayBands = memoize( async (stateCollateral: TAmount, userCollateral: TAmount, userBorrowed: TAmount, quote: IQuote, address: string): Promise<[bigint, bigint]> => { + address = _getAddress.call(this.llamalend, address); + if (!(await this.leverageRepayIsAvailable({ stateCollateral, userCollateral, userBorrowed, quote, address }))) return [parseUnits(0, 0), parseUnits(0, 0)]; + + const _stateRepayCollateral = parseUnits(stateCollateral, this.market.collateral_token.decimals); + const { _collateral: _stateCollateral, _debt: _stateDebt, _N } = await this.market._userState(address); + if (_stateDebt == BigInt(0)) throw Error(`Loan for ${address} does not exist`); + if (_stateCollateral < _stateRepayCollateral) throw Error(`Can't use more collateral than user's position has (${_stateRepayCollateral}) > ${_stateCollateral})`); + + let _n1 = parseUnits(0, 0); + let _n2 = parseUnits(0, 0); + const { _totalBorrowed: _repayExpected } = this._leverageRepayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed, quote); + try { + _n1 = await this.llamalend.contracts[this.market.addresses.controller].contract.calculate_debt_n1(_stateCollateral - _stateRepayCollateral, _stateDebt - _repayExpected, _N); + _n2 = _n1 + (_N - BigInt(1)); + return [_n2, _n1]; + } catch { + return [_n2, _n1]; + } + }, + { + promise: true, + maxAge: 5 * 60 * 1000, // 5m + }); + + + private async _leverageRepayHealth(stateCollateral: TAmount, userCollateral: TAmount, userBorrowed: TAmount, quote: IQuote, full = true, address = ""): Promise { + this._checkLeverageZap(); + address = _getAddress.call(this.llamalend, address); + const { _borrowed: _stateBorrowed, _debt, _N } = await this.market._userState(address); + if (_stateBorrowed > BigInt(0)) return "0.0"; + if (!(await this.leverageRepayIsAvailable({ stateCollateral, userCollateral, userBorrowed, quote, address }))) return "0.0"; + + const { _totalBorrowed } = this._leverageRepayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed, quote); + const _dCollateral = parseUnits(stateCollateral, this.market.collateral_token.decimals) * BigInt(-1); + const _dDebt = _totalBorrowed * BigInt(-1); + + if (_debt + _dDebt <= BigInt(0)) return "0.0"; + const contract = this.llamalend.contracts[this.market.addresses.controller].contract; + let _health = await contract.health_calculator(address, _dCollateral, _dDebt, full, _N, this.llamalend.constantOptions) as bigint; + _health = _health * BigInt(100); + + return this.llamalend.formatUnits(_health); + } + + public async leverageRepayIsApproved({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + return await hasAllowance.call(this.llamalend, + [this.market.collateral_token.address, this.market.borrowed_token.address], + [userCollateral, userBorrowed], + this.llamalend.signerAddress, + this.llamalend.constants.ALIASES.leverage_zap_v2 + ); + } + + public async leverageRepayApproveEstimateGas ({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + return await ensureAllowanceEstimateGas.call(this.llamalend, + [this.market.collateral_token.address, this.market.borrowed_token.address], + [userCollateral, userBorrowed], + this.llamalend.constants.ALIASES.leverage_zap_v2 + ); + } + + public async leverageRepayApprove({ userCollateral, userBorrowed }: { + userCollateral: TAmount, + userBorrowed: TAmount + }): Promise { + this._checkLeverageZap(); + return await ensureAllowance.call(this.llamalend, + [this.market.collateral_token.address, this.market.borrowed_token.address], + [userCollateral, userBorrowed], + this.llamalend.constants.ALIASES.leverage_zap_v2 + ); + } + + private async _leverageRepay( + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + router: string, + calldata: string, + estimateGas: boolean + ): Promise { + if (!(await this.market.userLoanExists())) throw Error("Loan does not exist"); + const _stateCollateral = parseUnits(stateCollateral, this.market.collateral_token.decimals); + const _userCollateral = parseUnits(userCollateral, this.market.collateral_token.decimals); + const _userBorrowed = parseUnits(userBorrowed, this.market.borrowed_token.decimals); + let zapCalldata = buildCalldataForLeverageZapV2(router, "0x"); + if (_stateCollateral + _userCollateral > BigInt(0)) { + zapCalldata = buildCalldataForLeverageZapV2(router, calldata) + } + + const contract = this.llamalend.contracts[this.market.addresses.controller].contract; + const gas = await contract.repay_extended.estimateGas( + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userCollateral, _userBorrowed], + zapCalldata + ); + if (estimateGas) return smartNumber(gas); + + await this.llamalend.updateFeeData(); + const gasLimit = _mulBy1_3(DIGas(gas)); + + return (await contract.repay_extended( + this.llamalend.constants.ALIASES.leverage_zap_v2, + [0, parseUnits(this._getMarketId(), 0), _userCollateral, _userBorrowed], + zapCalldata, + { ...this.llamalend.options, gasLimit } + )).hash + } + + public async leverageRepayEstimateGas({ stateCollateral, userCollateral, userBorrowed, router, calldata }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + if (!(await this.leverageRepayIsApproved({ userCollateral, userBorrowed }))) throw Error("Approval is needed for gas estimation"); + return await this._leverageRepay(stateCollateral, userCollateral, userBorrowed, router, calldata, true) as number; + } + + public async leverageRepay({ stateCollateral, userCollateral, userBorrowed, router, calldata }: { + stateCollateral: TAmount, + userCollateral: TAmount, + userBorrowed: TAmount, + router: string, + calldata: string + }): Promise { + this._checkLeverageZap(); + await this.leverageRepayApprove({ userCollateral, userBorrowed }); + return await this._leverageRepay(stateCollateral, userCollateral, userBorrowed, router, calldata, false) as string; + } +} \ No newline at end of file diff --git a/src/llamalend.ts b/src/llamalend.ts index dfa877c..a769ca2 100644 --- a/src/llamalend.ts +++ b/src/llamalend.ts @@ -348,6 +348,7 @@ class Llamalend implements ILlamalend { this.setContract(this.constants.ALIASES['one_way_factory'], OneWayLendingFactoryABI); this.setContract(this.constants.ALIASES['gauge_controller'], GaugeControllerABI); this.setContract(this.constants.ALIASES['leverage_zap'], LeverageZapABI); + this.setContract(this.constants.ALIASES['leverage_zap_v2'], LeverageZapABI); if (this.chainId === 1) { this.setContract(this.constants.ALIASES.minter, MinterABI); this.setContract(this.constants.ALIASES.gauge_factory, GaugeFactoryMainnetABI); diff --git a/src/utils.ts b/src/utils.ts index a43d190..05ecf04 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -515,4 +515,15 @@ export const calculateFutureLeverage = ( } else { return currentCollateralBN.minus(collateralBN).div(totalDepositBN.minus(collateralBN)).toString(); } -}; \ No newline at end of file +}; + +export const buildCalldataForLeverageZapV2 = (routerAddress: string, exchangeCalldata: string): string => { + const cleanCalldata = exchangeCalldata.startsWith('0x') ? exchangeCalldata.slice(2) : exchangeCalldata; + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + return abiCoder.encode( + ['address', 'bytes'], + [routerAddress, '0x' + cleanCalldata] + ); +}; +