From 79700fb593557d4e7280b891c49ae7af5e9ff08b Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:25:41 +0000 Subject: [PATCH] feat(liquidator): use close factor --- .../test/_initialization/initialize.test.ts | 95 ++++++++++--------- bots/bridge-tester/test/config.ts | 2 + bots/lending-liquidator/src/liquidate.ts | 57 ++++++----- .../test/integration/liquidate.test.ts | 2 +- docker-compose.yml | 2 +- 5 files changed, 84 insertions(+), 74 deletions(-) diff --git a/bots/bridge-tester/test/_initialization/initialize.test.ts b/bots/bridge-tester/test/_initialization/initialize.test.ts index 2f41562..821e5d8 100644 --- a/bots/bridge-tester/test/_initialization/initialize.test.ts +++ b/bots/bridge-tester/test/_initialization/initialize.test.ts @@ -1,11 +1,10 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { assert } from "chai"; -import BN from "bn.js"; import { BitcoinCoreClient, - setNumericStorage, + initializeStableConfirmations, issueSingle, InterBtcApi, DefaultInterBtcApi, @@ -24,19 +23,21 @@ import { DEFAULT_BITCOIN_CORE_PASSWORD, DEFAULT_BITCOIN_CORE_PORT, DEFAULT_BITCOIN_CORE_WALLET, + DEFAULT_SUDO_URI, + DEFAULT_USER_1_URI, } from "../config"; describe.skip("Initialize parachain state", () => { let api: ApiPromise; let bitcoinCoreClient: BitcoinCoreClient; let keyring: Keyring; - let interBtcAPI: InterBtcApi; + let userInterBtcApi: InterBtcApi; let wrappedCurrency: WrappedCurrency; let collateralCurrency: CollateralCurrencyExt; let vault_id: InterbtcPrimitivesVaultId; - let alice: KeyringPair; - let bob: KeyringPair; + let sudoAccount: KeyringPair; + let user1Account: KeyringPair; let charlie_stash: KeyringPair; function sleep(ms: number): Promise { @@ -47,9 +48,9 @@ describe.skip("Initialize parachain state", () => { api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); keyring = new Keyring({ type: "sr25519" }); // Alice is also the root account - alice = keyring.addFromUri("//Alice"); - bob = keyring.addFromUri("//Bob"); + user1Account = keyring.addFromUri(DEFAULT_USER_1_URI); charlie_stash = keyring.addFromUri("//Charlie//stash"); + sudoAccount = keyring.addFromUri(DEFAULT_SUDO_URI); bitcoinCoreClient = new BitcoinCoreClient( DEFAULT_BITCOIN_CORE_NETWORK, @@ -59,9 +60,9 @@ describe.skip("Initialize parachain state", () => { DEFAULT_BITCOIN_CORE_PORT, DEFAULT_BITCOIN_CORE_WALLET ); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", alice); - wrappedCurrency = interBtcAPI.getWrappedCurrency(); - collateralCurrency = interBtcAPI.api.consts.currency.getRelayChainCurrencyId; + userInterBtcApi = new DefaultInterBtcApi(api, "regtest", user1Account); + wrappedCurrency = userInterBtcApi.getWrappedCurrency(); + collateralCurrency = userInterBtcApi.api.consts.currency.getRelayChainCurrencyId; vault_id = newVaultId( api, charlie_stash.address, @@ -76,47 +77,49 @@ describe.skip("Initialize parachain state", () => { api.disconnect(); }); - it("should set the stable confirmations and ready the Btc Relay", async () => { - // Speed up the process by only requiring 0 parachain and 0 bitcoin confirmations - const stableBitcoinConfirmationsToSet = 0; - const stableParachainConfirmationsToSet = 0; - await setNumericStorage( - api, - "BTCRelay", - "StableBitcoinConfirmations", - new BN(stableBitcoinConfirmationsToSet), - alice - ); - await setNumericStorage( - api, - "BTCRelay", - "StableParachainConfirmations", - new BN(stableParachainConfirmationsToSet), - alice - ); - const stableBitcoinConfirmations = - await interBtcAPI.btcRelay.getStableBitcoinConfirmations(); - assert.equal( - stableBitcoinConfirmationsToSet, - stableBitcoinConfirmations, - "Setting the Bitcoin confirmations failed" - ); - const stableParachainConfirmations = - await interBtcAPI.btcRelay.getStableParachainConfirmations(); - assert.equal( - stableParachainConfirmationsToSet, - stableParachainConfirmations, - "Setting the Parachain confirmations failed" - ); - await bitcoinCoreClient.mineBlocks(3); - }); + it("should set the stable confirmations and ready the BTC-Relay", async () => { + // Speed up the process by only requiring 0 parachain and 0 bitcoin confirmations + const stableBitcoinConfirmationsToSet = 0; + const stableParachainConfirmationsToSet = 0; + let [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ + userInterBtcApi.btcRelay.getStableBitcoinConfirmations(), + userInterBtcApi.btcRelay.getStableParachainConfirmations(), + ]); + + if (stableBitcoinConfirmations != 0 || stableParachainConfirmations != 0) { + await initializeStableConfirmations( + api, + { + bitcoinConfirmations: stableBitcoinConfirmationsToSet, + parachainConfirmations: stableParachainConfirmationsToSet, + }, + sudoAccount, + bitcoinCoreClient + ); + [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ + userInterBtcApi.btcRelay.getStableBitcoinConfirmations(), + userInterBtcApi.btcRelay.getStableParachainConfirmations(), + ]); + } + assert.equal( + stableBitcoinConfirmationsToSet, + stableBitcoinConfirmations, + "Setting the Bitcoin confirmations failed" + ); + assert.equal( + stableParachainConfirmationsToSet, + stableParachainConfirmations, + "Setting the Parachain confirmations failed" + ); + }); + it("should issue 0.1 InterBTC", async () => { const wrappedToIssue = newMonetaryAmount(0.00007, wrappedCurrency, true); await issueSingle( - interBtcAPI, + userInterBtcApi, bitcoinCoreClient, - alice, + user1Account, wrappedToIssue, vault_id ); diff --git a/bots/bridge-tester/test/config.ts b/bots/bridge-tester/test/config.ts index c9e484e..f5568ea 100644 --- a/bots/bridge-tester/test/config.ts +++ b/bots/bridge-tester/test/config.ts @@ -6,3 +6,5 @@ export const DEFAULT_BITCOIN_CORE_PASSWORD = "rpcpassword"; export const DEFAULT_BITCOIN_CORE_PORT = "18443"; export const DEFAULT_BITCOIN_CORE_WALLET = "Alice"; export const DEFAULT_ISSUE_TOP_UP_AMOUNT = "0.1"; +export const DEFAULT_SUDO_URI = "//Alice"; +export const DEFAULT_USER_1_URI = "//Dave"; diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 4f55f63..672f8ee 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,14 +1,14 @@ -import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI } from "@interlay/interbtc-api"; +import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI, decodePermill } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; import { APPROX_BLOCK_TIME_MS } from "./consts"; type CollateralAndValue = { - collateral: CollateralPosition, + collateral: MonetaryAmount, referenceValue: MonetaryAmount } -function referencePrice(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { +function referenceValue(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { if (!rate) { return new MonetaryAmount(balance.currency, 0); } @@ -16,24 +16,30 @@ function referencePrice(balance: MonetaryAmount, rate: ExchangeRate return rate.toBase(balance) } -function findHighestValueCollateral(positions: CollateralPosition[], rates: Map>): CollateralAndValue | undefined { +function findHighestValueCollateral( + positions: CollateralPosition[], + rates: Map> +): CollateralAndValue | undefined { // It should be impossible to have no collateral currency locked, but just in case if (positions.length == 0) { return undefined; } const defaultValue = { - collateral: positions[0], - referenceValue: referencePrice(positions[0].amount, rates.get(positions[0].amount.currency)) + collateral: positions[0].amount, + referenceValue: referenceValue( + newMonetaryAmount(0, positions[0].amount.currency), + rates.get(positions[0].amount.currency) + ) }; return positions.reduce( (previous, current) => { - const currentReferencePrice = referencePrice(current.amount, rates.get(current.amount.currency)); - if (previous.referenceValue.gt(currentReferencePrice)) { + const liquidatableValue = referenceValue(current.amount, rates.get(current.amount.currency)); + if (previous.referenceValue.gt(liquidatableValue)) { return previous; } return { - collateral: current, - referenceValue: currentReferencePrice + collateral: current.amount, + referenceValue: liquidatableValue } }, defaultValue @@ -44,7 +50,8 @@ function liquidationStrategy( interBtcApi: InterBtcApi, liquidatorBalance: Map, oracleRates: Map>, - undercollateralizedBorrowers: UndercollateralizedPosition[] + undercollateralizedBorrowers: UndercollateralizedPosition[], + markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; @@ -55,19 +62,17 @@ function liquidationStrategy( } position.borrowPositions.forEach((loan) => { const totalDebt = loan.amount.add(loan.accumulatedDebt); - console.log("debt currency", totalDebt.currency); - console.log("highest value collateral ", highestValueCollateral.collateral.amount.currency.ticker, highestValueCollateral.referenceValue.toHuman()); if (liquidatorBalance.has(totalDebt.currency)) { + const loansMarket = markets.get(totalDebt.currency) as LoansMarket; + const closeFactor = decodePermill(loansMarket.closeFactor); const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; - console.log("free bot balance ", balance.free.toHuman()); - console.log("borrower debt", totalDebt.toHuman()); const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; - const repayableAmount = totalDebt.min(balance.free); - // TODO: Take close factor into account when consider the collateral's reference value - const referenceRepayable = referencePrice(repayableAmount, rate).min(highestValueCollateral.referenceValue); + // Can only repay a fraction of the total debt, defined by the `closeFactor` + const repayableAmount = totalDebt.mul(closeFactor).min(balance.free); + const referenceRepayable = referenceValue(repayableAmount, rate).min(highestValueCollateral.referenceValue); if (referenceRepayable.gt(maxRepayableLoan)) { maxRepayableLoan = referenceRepayable; - result = [repayableAmount, highestValueCollateral.collateral.amount.currency, position.accountId]; + result = [repayableAmount, highestValueCollateral.collateral.currency, position.accountId]; } } }) @@ -90,12 +95,12 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { console.log(`Scanning block: #${header.number}`); const liquidatorBalance: Map = new Map(); const oracleRates: Map> = new Map(); - console.log("awaiting big promise"); try { - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.loans.getLoansMarkets(), interBtcApi.assetRegistry.getForeignAssets() ]); @@ -104,7 +109,8 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { interBtcApi, liquidatorBalance, oracleRates, - undercollateralizedBorrowers + undercollateralizedBorrowers, + new Map(marketsArray) ) as [MonetaryAmount, CurrencyExt, AccountId]; if (potentialLiquidation) { const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; @@ -115,12 +121,11 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) ]); } - + + // Add any new foreign assets to `chainAssets` + chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); } catch (error) { console.log("found an error: ", error); } - - // Add any new foreign assets to `chainAssets` - chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); }); } diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index b6b1eca..dae9458 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -149,7 +149,7 @@ describe("liquidate", () => { const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); // crash the collateral exchange rate - const newExchangeRate = "0x00000000000000000000001000000000"; + const newExchangeRate = "0x00000000000000000000100000000000"; await setExchangeRate(sudoInterBtcAPI, depositAmount.currency, newExchangeRate); // expect liquidation event to happen diff --git a/docker-compose.yml b/docker-compose.yml index 2af20e0..b2e12f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,4 +105,4 @@ services: - --bitcoin-relay-start-height=1 environment: *client-env volumes: - - ./docker/vault_3-keyfile.json:/keyfile.json \ No newline at end of file + - ./docker/vault_3-keyfile.json:/keyfile.json