From 0c0d59c2b67d6527fa6c070714c46fadd0710956 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 5 Oct 2023 13:39:04 +0400 Subject: [PATCH 1/5] feat: deleverage --- src/constants/abis/DeleverageZap.json | 248 ++++++++++++++++++++++++++ src/constants/llammas.ts | 7 + src/crvusd.ts | 3 + src/interfaces.ts | 1 + src/llammas/LlammaTemplate.ts | 188 +++++++++++++++++++ 5 files changed, 447 insertions(+) create mode 100644 src/constants/abis/DeleverageZap.json diff --git a/src/constants/abis/DeleverageZap.json b/src/constants/abis/DeleverageZap.json new file mode 100644 index 0000000..ee2e2cd --- /dev/null +++ b/src/constants/abis/DeleverageZap.json @@ -0,0 +1,248 @@ +[ + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [ + { + "name": "_controller", + "type": "address" + }, + { + "name": "_collateral", + "type": "address" + }, + { + "name": "_router", + "type": "address" + }, + { + "name": "_routes", + "type": "address[11][]" + }, + { + "name": "_route_params", + "type": "uint256[5][5][]" + }, + { + "name": "_route_pools", + "type": "address[5][]" + }, + { + "name": "_route_names", + "type": "string[]" + } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_stablecoins", + "inputs": [ + { + "name": "collateral", + "type": "uint256" + }, + { + "name": "route_idx", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "calculate_debt_n1", + "inputs": [ + { + "name": "collateral", + "type": "uint256" + }, + { + "name": "route_idx", + "type": "uint256" + }, + { + "name": "user", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "int256" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "callback_repay", + "inputs": [ + { + "name": "user", + "type": "address" + }, + { + "name": "stablecoins", + "type": "uint256" + }, + { + "name": "collateral", + "type": "uint256" + }, + { + "name": "debt", + "type": "uint256" + }, + { + "name": "callback_args", + "type": "uint256[]" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[2]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "CONTROLLER", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "COLLATERAL", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ROUTER", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "routes", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + }, + { + "name": "arg1", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "route_params", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + }, + { + "name": "arg1", + "type": "uint256" + }, + { + "name": "arg2", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "route_pools", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + }, + { + "name": "arg1", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "route_names", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "string" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "routes_count", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + } +] \ No newline at end of file diff --git a/src/constants/llammas.ts b/src/constants/llammas.ts index 4a4dfbd..77aba4a 100644 --- a/src/constants/llammas.ts +++ b/src/constants/llammas.ts @@ -3,6 +3,7 @@ import MonetaryPolicyABI from "../constants/abis/MonetaryPolicy.json"; import MonetaryPolicy2ABI from "../constants/abis/MonetaryPolicy2.json"; import { lowerCaseLlammasAddresses } from "./utils"; + export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ sfrxeth: { amm_address: '0x136e783846ef68C8Bd00a3369F787dF8d683a696', @@ -10,6 +11,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0xc684432FD6322c6D58b6bC5d28B18569aA0AD0A1', collateral_address: '0xac3E018457B222d93114458476f3E3416Abbe38F', leverage_zap: '0xb556FA4C4752321B3154f08DfBDFCF34847f2eac', + deleverage_zap: '0xF113929F69FAbE165A2280CaC00c5f77196Aa34C', collateral_symbol: 'sfrxETH', collateral_decimals: 18, min_bands: 4, @@ -24,6 +26,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0x1E7d3bf98d3f8D8CE193236c3e0eC4b00e32DaaE', collateral_address: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', leverage_zap: '0x293436d4e4a15FBc6cCC400c14a01735E5FC74fd', + deleverage_zap: '0x600E571106C31c4Ca1bF4177bA808E37146A4A0C', collateral_symbol: 'wstETH', collateral_decimals: 18, min_bands: 4, @@ -38,6 +41,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0x1E7d3bf98d3f8D8CE193236c3e0eC4b00e32DaaE', collateral_address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', leverage_zap: '0xA2518b71ee64E910741f5Cf480b19E8e402de4d7', + deleverage_zap: '0xb911D7e59BA82FDF477a2Ab22Ff25125072C9282', health_calculator_zap: "0xCF61Ee62b136e3553fB545bd8fEc11fb7f830d6A", collateral_symbol: 'WBTC', collateral_decimals: 8, @@ -53,6 +57,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0x1E7d3bf98d3f8D8CE193236c3e0eC4b00e32DaaE', collateral_address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', leverage_zap: '0xd3e576B5DcDe3580420A5Ef78F3639BA9cd1B967', + deleverage_zap: '0x9bE82CdDB5c266E010C97e4B1B5B2DF53C16384d', collateral_symbol: 'ETH', collateral_decimals: 18, min_bands: 4, @@ -67,6 +72,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0x1e7d3bf98d3f8d8ce193236c3e0ec4b00e32daae', collateral_address: '0xac3e018457b222d93114458476f3e3416abbe38f', leverage_zap: '0x43eCFfe6c6C1b9F24AeB5C180E659c2a6FCe11Bc', + deleverage_zap: '0x2bc706B83aB08d0437b8A397242C3284B5f81D74', collateral_symbol: 'sfrxETH', collateral_decimals: 18, min_bands: 4, @@ -81,6 +87,7 @@ export const LLAMMAS: IDict = lowerCaseLlammasAddresses({ monetary_policy_address: '0xb8687d7dc9d8fa32fabde63e19b2dbc9bb8b2138', collateral_address: '0x18084fba666a33d37592fa2633fd49a74dd93a88', leverage_zap: '0xD79964C70Cb06224FdA4c48387B53E9819bcB71c', + deleverage_zap: '0xAA25a6Fa9e4dADaE0d3EE59bEA19fbcf0284830C', collateral_symbol: 'tBTC', collateral_decimals: 18, min_bands: 4, diff --git a/src/crvusd.ts b/src/crvusd.ts index 1fabf5e..c5d9d0f 100644 --- a/src/crvusd.ts +++ b/src/crvusd.ts @@ -9,6 +9,7 @@ import controllerABI from "./constants/abis/controller.json"; import llammaABI from "./constants/abis/llamma.json"; import HealthCalculatorZapABI from "./constants/abis/HealthCalculatorZap.json"; import LeverageZapABI from "./constants/abis/LeverageZap.json"; +import DeleverageZapABI from "./constants/abis/DeleverageZap.json"; import PegKeeper from "./constants/abis/PegKeeper.json"; import { LLAMMAS } from "./constants/llammas"; import { COINS } from "./constants/coins"; @@ -147,6 +148,7 @@ class Crvusd implements Icrvusd { this.setContract(llamma.collateral_address, ERC20ABI); } this.setContract(llamma.leverage_zap, LeverageZapABI); + this.setContract(llamma.deleverage_zap, DeleverageZapABI); if (llamma.health_calculator_zap) this.setContract(llamma.health_calculator_zap, HealthCalculatorZapABI); } for (const pegKeeper of this.constants.PEG_KEEPERS) { @@ -206,6 +208,7 @@ class Crvusd implements Icrvusd { monetary_policy_address, collateral_address: is_eth ? "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" : collaterals[i], leverage_zap: "0x0000000000000000000000000000000000000000", + deleverage_zap: "0x0000000000000000000000000000000000000000", collateral_symbol: is_eth ? "ETH" : collateral_symbol, collateral_decimals, min_bands: 4, diff --git a/src/interfaces.ts b/src/interfaces.ts index 741ee8b..ab5486d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -11,6 +11,7 @@ export interface ILlamma { monetary_policy_address: string, collateral_address: string, leverage_zap: string, + deleverage_zap: string, health_calculator_zap?: string, collateral_symbol: string, collateral_decimals: number, diff --git a/src/llammas/LlammaTemplate.ts b/src/llammas/LlammaTemplate.ts index 84d1be9..263f068 100644 --- a/src/llammas/LlammaTemplate.ts +++ b/src/llammas/LlammaTemplate.ts @@ -28,6 +28,7 @@ export class LlammaTemplate { monetaryPolicy: string; collateral: string; leverageZap: string; + deleverageZap: string; healthCalculator: string | undefined; collateralSymbol: string; collateralDecimals: number; @@ -100,6 +101,20 @@ export class LlammaTemplate { createLoan: (collateral: number | string, debt: number | string, range: number, slippage?: number) => Promise, } } + deleverage: { + repayStablecoins: (collateral: number | string) => Promise<{ stablecoins: string, routeIdx: number }>, + getRouteName: (routeIdx: number) => Promise, + isAvailable: (deleverageCollateral: number | string, address?: string) => Promise, + isFullRepayment: (deleverageCollateral: number | string, address?: string) => Promise, + repayBands: (collateral: number | string, address?: string) => Promise<[number, number]>, + repayPrices: (collateral: number | string, address?: string) => Promise, + repayHealth: (collateral: number | string, full?: boolean, address?: string) => Promise, + repay: (collateral: number | string, slippage?: number) => Promise, + priceImpact: (collateral: number | string) => Promise, + estimateGas: { + repay: (collateral: number | string, slippage?: number) => Promise, + } + } constructor(id: string) { const llammaData = crvusd.constants.LLAMMAS[id]; @@ -110,6 +125,7 @@ export class LlammaTemplate { this.monetaryPolicy = llammaData.monetary_policy_address; this.collateral = llammaData.collateral_address; this.leverageZap = llammaData.leverage_zap; + this.deleverageZap = llammaData.deleverage_zap; this.healthCalculator = llammaData.health_calculator_zap; this.collateralSymbol = llammaData.collateral_symbol; this.collateralDecimals = llammaData.collateral_decimals; @@ -176,6 +192,20 @@ export class LlammaTemplate { createLoan: this.leverageCreateLoanEstimateGas.bind(this), }, } + this.deleverage = { + repayStablecoins: this.deleverageRepayStablecoins.bind(this), + getRouteName: this.deleverageGetRouteName.bind(this), + isAvailable: this.deleverageIsAvailable.bind(this), + isFullRepayment: this.deleverageIsFullRepayment.bind(this), + repayBands: this.deleverageRepayBands.bind(this), + repayPrices: this.deleverageRepayPrices.bind(this), + repayHealth: this.deleverageRepayHealth.bind(this), + priceImpact: this.deleveragePriceImpact.bind(this), + repay: this.deleverageRepay.bind(this), + estimateGas: { + repay: this.deleverageRepayEstimateGas.bind(this), + }, + } } // ---------------- STATS ---------------- @@ -1590,4 +1620,162 @@ export class LlammaTemplate { await this.createLoanApprove(collateral); return await this._leverageCreateLoan(collateral, debt, range, slippage, false) as string; } + + // ---------------- DELEVERAGE REPAY ---------------- + + private _checkDeleverageZap(): void { + if (this.deleverageZap === "0x0000000000000000000000000000000000000000") throw Error(`There is no deleverage for ${this.id} market`) + } + + private deleverageRepayStablecoins = memoize( async (collateral: number | string): Promise<{ stablecoins: string, routeIdx: number }> => { + this._checkDeleverageZap(); + const _collateral = parseUnits(collateral, this.collateralDecimals); + const calls = []; + for (let i = 0; i < 5; i++) { + calls.push(crvusd.contracts[this.deleverageZap].multicallContract.get_stablecoins(_collateral, i)); + } + const _stablecoins_arr: ethers.BigNumber[] = await crvusd.multicallProvider.all(calls); + const routeIdx = this._getBestIdx(_stablecoins_arr); + const stablecoins = crvusd.formatUnits(_stablecoins_arr[routeIdx]); + + return { stablecoins, routeIdx }; + }, + { + promise: true, + maxAge: 5 * 60 * 1000, // 5m + }); + + private async deleverageGetRouteName(routeIdx: number): Promise { + this._checkDeleverageZap(); + return await crvusd.contracts[this.deleverageZap].contract.route_names(routeIdx); + } + + private async deleverageIsFullRepayment(deleverageCollateral: number | string, address = ""): Promise { + address = _getAddress(address); + const { stablecoin, debt } = await this.userState(address); + const { stablecoins: deleverageStablecoins } = await this.deleverageRepayStablecoins(deleverageCollateral); + + return BN(stablecoin).plus(deleverageStablecoins).gt(debt); + } + + private async deleverageIsAvailable(deleverageCollateral: number | string, address = ""): Promise { + // 0. const { collateral, stablecoin, debt } = await this.userState(address); + // 1. maxCollateral for deleverage is collateral from line above (0). + // 2. If user is underwater (stablecoin > 0), only full repayment is available: + // await this.deleverageRepayStablecoins(deleverageCollateral) + stablecoin > debt + + // There is no deleverage zap + if (this.deleverageZap === "0x0000000000000000000000000000000000000000") return false; + + address = _getAddress(address); + const { collateral, stablecoin, debt } = await this.userState(address); + // Loan does not exist + if (BN(debt).eq(0)) return false; + // Can't spend more than user has + if (BN(deleverageCollateral).gt(collateral)) return false; + // Only full repayment and closing the position is available if user is underwater+ + if (BN(stablecoin).gt(0)) return await this.deleverageIsFullRepayment(deleverageCollateral, address); + + return true; + } + + private _deleverageRepayBands = memoize( async (collateral: number | string, address: string): Promise<[ethers.BigNumber, ethers.BigNumber]> => { + address = _getAddress(address); + if (!(await this.deleverageIsAvailable(collateral, address))) return [parseUnits(0, 0), parseUnits(0, 0)]; + const { routeIdx } = await this.deleverageRepayStablecoins(collateral); + const { _debt: _currentDebt } = await this._userState(address); + if (_currentDebt.eq(0)) throw Error(`Loan for ${address} does not exist`); + + const N = await this.userRange(address); + const _collateral = parseUnits(collateral, this.collateralDecimals); + let _n1 = parseUnits(0, 0); + let _n2 = parseUnits(0, 0); + try { + _n1 = await crvusd.contracts[this.deleverageZap].contract.calculate_debt_n1(_collateral, routeIdx, address); + _n2 = _n1.add(N - 1); + } catch (e) { + console.log("Full repayment"); + } + + return [_n2, _n1]; + }, + { + promise: true, + maxAge: 5 * 60 * 1000, // 5m + }); + + private async deleverageRepayBands(collateral: number | string, address = ""): Promise<[number, number]> { + this._checkDeleverageZap(); + const [_n2, _n1] = await this._deleverageRepayBands(collateral, address); + + return [_n2.toNumber(), _n1.toNumber()]; + } + + private async deleverageRepayPrices(debt: number | string, address = ""): Promise { + this._checkDeleverageZap(); + const [_n2, _n1] = await this._deleverageRepayBands(debt, address); + + return await this._getPrices(_n2, _n1); + } + + private async deleverageRepayHealth(collateral: number | string, full = true, address = ""): Promise { + this._checkDeleverageZap(); + address = _getAddress(address); + if (!(await this.deleverageIsAvailable(collateral, address))) return "0.0"; + const { _stablecoin, _debt } = await this._userState(address); + const { stablecoins: deleverageStablecoins } = await this.deleverageRepayStablecoins(collateral); + const _d_collateral = parseUnits(collateral, this.collateralDecimals).mul(-1); + const _d_debt = parseUnits(deleverageStablecoins).add(_stablecoin).mul(-1); + const N = await this.userRange(address); + + if (_debt.add(_d_debt).lt(0)) return "0.0"; + const contract = crvusd.contracts[this.healthCalculator ?? this.controller].contract; + let _health = await contract.health_calculator(address, _d_collateral, _d_debt, full, N, crvusd.constantOptions) as ethers.BigNumber; + _health = _health.mul(100); + + return ethers.utils.formatUnits(_health); + } + + public async deleveragePriceImpact(collateral: number | string): Promise { + const x_BN = BN(collateral); + const small_x_BN = BN(0.001); + const { stablecoins, routeIdx } = await this.deleverageRepayStablecoins(collateral); + const _y = parseUnits(stablecoins); + const _small_y = await crvusd.contracts[this.deleverageZap].contract.get_stablecoins(fromBN(small_x_BN, this.collateralDecimals), routeIdx); + const y_BN = toBN(_y); + const small_y_BN = toBN(_small_y); + const rateBN = y_BN.div(x_BN); + const smallRateBN = small_y_BN.div(small_x_BN); + if (rateBN.gt(smallRateBN)) return "0.0"; + + return BN(1).minus(rateBN.div(smallRateBN)).times(100).toFixed(4); + } + + private async _deleverageRepay(collateral: number | string, slippage: number, estimateGas: boolean): Promise { + const { debt: currentDebt } = await this.userState(crvusd.signerAddress); + if (Number(currentDebt) === 0) throw Error(`Loan for ${crvusd.signerAddress} does not exist`); + + const { stablecoins, routeIdx } = await this.deleverageRepayStablecoins(collateral); + const _collateral = parseUnits(collateral, this.collateralDecimals); + const _debt = parseUnits(stablecoins); + const minRecvBN = toBN(_debt).times(100 - slippage).div(100); + const _minRecv = fromBN(minRecvBN); + const contract = crvusd.contracts[this.controller].contract; + const gas = await contract.estimateGas.repay_extended(this.deleverageZap, [routeIdx, _collateral, _minRecv], crvusd.constantOptions); + if (estimateGas) return gas.toNumber(); + + await crvusd.updateFeeData(); + const gasLimit = gas.mul(130).div(100); + return (await contract.repay_extended(this.deleverageZap, [routeIdx, _collateral, _minRecv], { ...crvusd.options, gasLimit })).hash + } + + private async deleverageRepayEstimateGas(collateral: number | string, slippage = 0.5): Promise { + this._checkDeleverageZap(); + return await this._deleverageRepay(collateral, slippage, true) as number; + } + + private async deleverageRepay(collateral: number | string, slippage = 0.5): Promise { + this._checkDeleverageZap(); + return await this._deleverageRepay(collateral, slippage, false) as string; + } } From c3ccc01af9d35ca617e3729e00082c48944c4ded Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 5 Oct 2023 13:40:11 +0400 Subject: [PATCH 2/5] test: deleverage --- test/general.test.ts | 80 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/test/general.test.ts b/test/general.test.ts index d63daec..c283c3f 100644 --- a/test/general.test.ts +++ b/test/general.test.ts @@ -201,10 +201,9 @@ const generalTest = (id: string) => { it('Leverage', async function () { const initialBalances = await llamma.wallet.balances(); const initialState = await llamma.userState(); + const loanExists = await llamma.loanExists(); - assert.equal(Number(initialState.collateral), 0); - assert.equal(Number(initialState.stablecoin), 0); - assert.equal(Number(initialState.debt), 0); + assert.isFalse(loanExists); assert.isAbove(Number(initialBalances.collateral), 0); const collateralAmount = 0.5; @@ -227,17 +226,86 @@ const generalTest = (id: string) => { assert.approximately(Number(createLoanPrices[0]), Number(userPrices[0]), 1e-2, 'price 0'); assert.approximately(Number(createLoanPrices[1]), Number(userPrices[1]), 1e-2, 'price 1'); assert.approximately(Number(createLoanFullHealth), Number(fullHealth), 0.1, 'full health'); - assert.approximately(Number(createLoanHealth), Number(health), 1e-4, 'health'); + assert.approximately(Number(createLoanHealth), Number(health), 1e-3, 'health'); assert.equal(Number(balances.collateral), Number(initialBalances.collateral) - Number(collateralAmount), 'wallet collateral'); assert.equal(Number(balances.stablecoin), Number(initialBalances.stablecoin), 'wallet stablecoin'); - assert.approximately(Number(state.collateral), Number(collateral), 1e-7, 'state collateral'); + assert.approximately(Number(state.collateral), Number(collateral), 1e-6, 'state collateral'); assert.equal(Number(state.debt), Number(debtAmount), 'state debt'); }); + + it('Deleverage', async function () { + const initialBalances = await llamma.wallet.balances(); + const initialState = await llamma.userState(); + const loanExists = await llamma.loanExists(); + const collateralAmount = Number(initialState.collateral) / 10; + const deleverageIsAvailable = await llamma.deleverage.isAvailable(collateralAmount); + const isFullRepayment = await llamma.deleverage.isFullRepayment(collateralAmount); + + assert.isTrue(loanExists); + assert.isTrue(deleverageIsAvailable); + assert.isFalse(isFullRepayment); + + const deleverageBands = await llamma.deleverage.repayBands(collateralAmount); + const deleveragePrices = await llamma.deleverage.repayPrices(collateralAmount); + const deleverageFullHealth = await llamma.deleverage.repayHealth(collateralAmount); + const deleverageHealth = await llamma.deleverage.repayHealth(collateralAmount, false); + const { stablecoins } = await llamma.deleverage.repayStablecoins(collateralAmount); + + await llamma.deleverage.repay(collateralAmount); + + const balances = await llamma.wallet.balances(); + const state = await llamma.userState(); + const userBands = await llamma.userBands(); + const userPrices = await llamma.userPrices(); + const fullHealth = await llamma.userHealth(); + const health = await llamma.userHealth(false); + + + assert.equal(deleverageBands[0], userBands[0], 'band 0'); + assert.equal(deleverageBands[1], userBands[1], 'band 1'); + assert.approximately(Number(deleveragePrices[0]), Number(userPrices[0]), 0.1, 'price 0'); + assert.approximately(Number(deleveragePrices[1]), Number(userPrices[1]), 0.1, 'price 1'); + assert.approximately(Number(deleverageFullHealth), Number(fullHealth), 0.1, 'full health'); + assert.approximately(Number(deleverageHealth), Number(health), 1e-4, 'health'); + assert.equal(balances.collateral, initialBalances.collateral, 'wallet collateral'); + assert.equal(balances.stablecoin, initialBalances.stablecoin, 'wallet stablecoin'); + assert.approximately(Number(state.collateral), Number(initialState.collateral) - collateralAmount, 1e-5, 'state collateral'); + assert.approximately(Number(state.debt), Number(initialState.debt) - Number(stablecoins), 1e-2, 'state debt'); + }); + + it('Full deleverage', async function () { + const initialBalances = await llamma.wallet.balances(); + const initialState = await llamma.userState(); + const initialLoanExists = await llamma.loanExists(); + const collateralAmount = Number(initialState.collateral) * 0.95; + const deleverageIsAvailable = await llamma.deleverage.isAvailable(collateralAmount); + const isFullRepayment = await llamma.deleverage.isFullRepayment(collateralAmount); + + assert.isTrue(initialLoanExists); + assert.isTrue(deleverageIsAvailable); + assert.isTrue(isFullRepayment); + + const { stablecoins } = await llamma.deleverage.repayStablecoins(collateralAmount); + + await llamma.deleverage.repay(collateralAmount); + + const balances = await llamma.wallet.balances(); + const loanExists = await llamma.loanExists(); + + + if (llamma.id !== "eth") { + assert.approximately(Number(balances.collateral), Number(initialBalances.collateral) + Number(initialState.collateral) - collateralAmount, + 1e-5, 'wallet collateral'); + } + assert.approximately(Number(balances.stablecoin), Number(initialBalances.stablecoin) + Number(stablecoins) - Number(initialState.debt), + 1e-3, 'wallet stablecoin'); + assert.isFalse(loanExists, 'loan exists'); + }); }) } describe('General test', async function () { - this.timeout(240000); + this.timeout(120000); before(async function () { await crvusd.init('JsonRpc', {},{ gasPrice: 0, maxFeePerGas: 0, maxPriorityFeePerGas: 0 }); From 8cdc84109b0ab7065032152e37dee24cffebc52f Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 5 Oct 2023 13:41:19 +0400 Subject: [PATCH 3/5] fix: _leverageCreateLoanCollateral --- src/llammas/LlammaTemplate.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/llammas/LlammaTemplate.ts b/src/llammas/LlammaTemplate.ts index 263f068..d9533f1 100644 --- a/src/llammas/LlammaTemplate.ts +++ b/src/llammas/LlammaTemplate.ts @@ -1412,10 +1412,8 @@ export class LlammaTemplate { const _userCollateral = parseUnits(userCollateral, this.collateralDecimals); const _debt = parseUnits(debt); const calls = []; - for (let N = this.minBands; N <= this.maxBands; N++) { - for (let i = 0; i < 5; i++) { - calls.push(crvusd.contracts[this.leverageZap].multicallContract.get_collateral(_debt, i)); - } + for (let i = 0; i < 5; i++) { + calls.push(crvusd.contracts[this.leverageZap].multicallContract.get_collateral(_debt, i)); } const _leverageCollateral: ethers.BigNumber[] = await crvusd.multicallProvider.all(calls); const routeIdx = this._getBestIdx(_leverageCollateral); From 851d82a65f7d5e7a71bd471862024a93aec86283 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 5 Oct 2023 19:56:36 +0400 Subject: [PATCH 4/5] docs: deleverage --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++--- test/readme.test.ts | 29 +++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0d03bff..fcd690a 100644 --- a/README.md +++ b/README.md @@ -676,16 +676,16 @@ import crvusd from "@curvefi/stablecoin-api"; // | // collateral | crvUSD crvUSD // user --> controller --> leverage_zap --> curve_router - // ^ | - // |_____________________________________________| - // leverage_collateral + // ^ | ^ | + // |______________________| |___________________| + // leverage_collateral leverage_collateral await crvusd.init('JsonRpc', {}); const llamma = crvusd.getLlamma('wsteth'); - await llamma.leverage.createLoanMaxRecv(0.5, 5); + await llamma.leverage.createLoanMaxRecv(1, 5); // { // maxBorrowable: '16547.886068664425693035', // maxCollateral: '8.789653769216069731', @@ -842,6 +842,76 @@ import crvusd from "@curvefi/stablecoin-api"; // } ``` +### Deleverage +```ts +(async () => { + + // Deleveraged position (fully or partially) + // ^ + // | + // | collateral collateral + // controller --> leverage_zap --> curve_router + // ^ | ^ | + // |______________________| |___________________| + // crvUSD crvUSD + + await crvusd.init('JsonRpc', {}); + + const llamma = crvusd.getLlamma('wsteth'); + + + await llamma.userState(); + // { + // collateral: '1.532865973844812038', + // stablecoin: '0.0', + // debt: '1000.0' + // } + const { stablecoins, routeIdx } = await llamma.deleverage.repayStablecoins(0.5); + // { stablecoins: '936.993512434228957835', routeIdx: 2 } + await llamma.deleverage.getRouteName(routeIdx) + // wstETH wrapper -> steth -> factory-tricrypto-4 (TriCRV) + await llamma.deleverage.repayBands(0.5) + // [ 344, 340 ] + await llamma.deleverage.repayPrices(0.5) + // [ '65.389368517832066821', '68.759256234814550815' ] + await llamma.deleverage.repayHealth(0.5) // FULL + // 2962.6116372201716629 + await llamma.deleverage.repayHealth(0.5, false) // NOT FULL + // 3.3355078309621505 + await llamma.deleverage.priceImpact(0.5) + // 0.0080 % + await llamma.deleverage.isAvailable(0.5) + // true + await llamma.deleverage.isFullRepayment(0.5) + // false + + await llamma.deleverage.repay(0.5, 0.3) + + await llamma.userState() + // { + // collateral: '1.032865973844812038', + // stablecoin: '0.0', + // debt: '63.006629410173187253' + // } + await llamma.userBands() + // [ 344, 340 ] + await llamma.userPrices() + // [ '65.389377792947951092', '68.759265987930143609' ] + await llamma.userHealth() // FULL + // 2962.6210276926274746 + await llamma.userHealth(false) // NOT FULL + // 3.3352898532375197 + await llamma.userBandsBalances() + // { + // '340': { stablecoin: '0.0', collateral: '0.20657319476896241' }, + // '341': { stablecoin: '0.0', collateral: '0.206573194768962407' }, + // '342': { stablecoin: '0.0', collateral: '0.206573194768962407' }, + // '343': { stablecoin: '0.0', collateral: '0.206573194768962407' }, + // '344': { stablecoin: '0.0', collateral: '0.206573194768962407' } + // } +})() +``` + ## Gas estimation Every non-constant method has corresponding gas estimation method. Rule: ```obj.method -> obj.estimateGas.method``` diff --git a/test/readme.test.ts b/test/readme.test.ts index 86d4a46..9d98678 100644 --- a/test/readme.test.ts +++ b/test/readme.test.ts @@ -322,6 +322,33 @@ const leverageAllRangesTest = async () => { console.log(await llamma.leverage.createLoanPricesAllRanges(1, 14000)); } +const deleverageTest = async () => { + await crvusd.init('JsonRpc', {}); + + const llamma = crvusd.getLlamma('wsteth'); + + console.log(await llamma.userState()); + const { stablecoins, routeIdx } = await llamma.deleverage.repayStablecoins(0.5); + console.log({ stablecoins, routeIdx }); + console.log(await llamma.deleverage.getRouteName(routeIdx)); + console.log(await llamma.deleverage.repayBands(0.5)); + console.log(await llamma.deleverage.repayPrices(0.5)); + console.log(await llamma.deleverage.repayHealth(0.5)); // FULL + console.log(await llamma.deleverage.repayHealth(0.5, false)); // NOT FULL + console.log(await llamma.deleverage.priceImpact(0.5)); + console.log(await llamma.deleverage.isAvailable(0.5)); + console.log(await llamma.deleverage.isFullRepayment(0.5)); + + console.log(await llamma.deleverage.repay(0.5, 0.3)); + + console.log(await llamma.userState()); + console.log(await llamma.userBands()); + console.log(await llamma.userPrices()); + console.log(await llamma.userHealth()); // FULL + console.log(await llamma.userHealth(false)); // NOT FULL + console.log(await llamma.userBandsBalances()); +} + (async () => { console.log("\n--- generalMethodsTest ---\n") await generalMethodsTest(); @@ -345,4 +372,6 @@ const leverageAllRangesTest = async () => { await leverageTest(); console.log("\n--- leverageAllRangesTest ---\n") await leverageAllRangesTest(); + console.log("\n--- deleverageTest ---\n") + await deleverageTest(); })() From 8d34ce935ec232a970de83019a682c08ac3433f3 Mon Sep 17 00:00:00 2001 From: macket Date: Thu, 5 Oct 2023 20:53:55 +0400 Subject: [PATCH 5/5] build: v1.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a8c26c..e432689 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@curvefi/stablecoin-api", - "version": "1.4.0", + "version": "1.5.0", "description": "JavaScript library for Curve Stablecoin", "main": "lib/index.js", "author": "Macket",