Skip to content

Commit

Permalink
feat(liquidator): strategy improvements, use instant seal
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-savu committed Mar 8, 2023
1 parent ce5e5c0 commit fdc8ab9
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 65 deletions.
3 changes: 2 additions & 1 deletion bots/lending-liquidator/src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { InterBtc, Interlay, KBtc, Kintsugi, Kusama, Polkadot } from "@interlay/monetary-js";

export const NATIVE_CURRENCIES = [Kintsugi, Kusama, KBtc, Interlay, Polkadot, InterBtc];
// approximate time per block in ms
export const APPROX_BLOCK_TIME_MS = 12 * 1000;
1 change: 1 addition & 0 deletions bots/lending-liquidator/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { startLiquidator } from "./liquidate";
69 changes: 44 additions & 25 deletions bots/lending-liquidator/src/liquidate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition } from "@interlay/interbtc-api";
import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI } from "@interlay/interbtc-api";
import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js";
import { AccountId } from "@polkadot/types/interfaces";
import { NATIVE_CURRENCIES } from "./consts";
import { APPROX_BLOCK_TIME_MS } from "./consts";

type CollateralAndValue = {
collateral: CollateralPosition,
Expand All @@ -28,7 +28,7 @@ function findHighestValueCollateral(positions: CollateralPosition[], rates: Map<
return positions.reduce(
(previous, current) => {
const currentReferencePrice = referencePrice(current.amount, rates.get(current.amount.currency));
if (previous.collateral.amount.gt(currentReferencePrice)) {
if (previous.referenceValue.gt(currentReferencePrice)) {
return previous;
}
return {
Expand All @@ -42,7 +42,6 @@ function findHighestValueCollateral(positions: CollateralPosition[], rates: Map<

function liquidationStrategy(
interBtcApi: InterBtcApi,
chainAssets: Set<CurrencyExt>,
liquidatorBalance: Map<Currency, ChainBalance>,
oracleRates: Map<Currency, ExchangeRate<Currency, CurrencyExt>>,
undercollateralizedBorrowers: UndercollateralizedPosition[]
Expand All @@ -55,10 +54,15 @@ function liquidationStrategy(
return;
}
position.borrowPositions.forEach((loan) => {
if (chainAssets.has(loan.accumulatedDebt.currency)) {
const balance = liquidatorBalance.get(loan.accumulatedDebt.currency) as ChainBalance;
const rate = oracleRates.get(loan.accumulatedDebt.currency) as ExchangeRate<Currency, CurrencyExt>;
const repayableAmount = loan.accumulatedDebt.min(balance.free);
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 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<Currency, CurrencyExt>;
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);
if (referenceRepayable.gt(maxRepayableLoan)) {
Expand All @@ -71,37 +75,52 @@ function liquidationStrategy(
return result;
}

async function start(interBtcApi: InterBtcApi): Promise<void> {
export async function startLiquidator(interBtcApi: InterBtcApi): Promise<void> {
console.log("Starting lending liquidator...");
const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets();
let chainAssets = new Set([...NATIVE_CURRENCIES, ...foreignAssets]);
if (!interBtcApi.account) {

let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()]
let chainAssets = new Set([...nativeCurrency, ...foreignAssets]);
if (interBtcApi.account == undefined) {
return Promise.reject("No account set for the lending-liquidator");
}
const accountId = newAccountId(interBtcApi.api, interBtcApi.account.toString());
const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account);
console.log("Listening to new blocks...");
await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => {

console.log(`Scanning block: #${header.number}`);
const liquidatorBalance: Map<Currency, ChainBalance> = new Map();
const oracleRates: Map<Currency, ExchangeRate<Currency, CurrencyExt>> = new Map();
const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, 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.assetRegistry.getForeignAssets()
]);

if (undercollateralizedBorrowers.length > 0) {
const [amountToRepay, collateralToLiquidate, borrower] = liquidationStrategy(
console.log("awaiting big promise");
try {
const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, 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.assetRegistry.getForeignAssets()
]);

console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`);
const potentialLiquidation = liquidationStrategy(
interBtcApi,
chainAssets,
liquidatorBalance,
oracleRates,
undercollateralizedBorrowers
) as [MonetaryAmount<CurrencyExt>, CurrencyExt, AccountId];
await interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate);
if (potentialLiquidation) {
const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation;
console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`);
// Either our liquidation will go through, or someone else's will
await Promise.all([
DefaultTransactionAPI.waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS),
interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate)
]);
}

} catch (error) {
console.log("found an error: ", error);
}

// Add any new foreign assets to `chainAssets`
chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]);
console.log(`Scanned block: #${header.number}`);
});
}
3 changes: 0 additions & 3 deletions bots/lending-liquidator/test/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,3 @@ export const DEFAULT_PARACHAIN_ENDPOINT = "ws://127.0.0.1:9944";
export const DEFAULT_SUDO_URI = "//Alice";
export const DEFAULT_USER_1_URI = "//Dave";
export const DEFAULT_USER_2_URI = "//Eve";

// approximate time per block in ms
export const APPROX_BLOCK_TIME_MS = 12 * 1000;
16 changes: 9 additions & 7 deletions bots/lending-liquidator/test/integration/liquidate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { KeyringPair } from "@polkadot/keyring/types";
import { AccountId } from "@polkadot/types/interfaces";
import { expect } from "chai";

import { APPROX_BLOCK_TIME_MS, DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config";
import { setExchangeRate, waitForNthBlock, waitRegisteredLendingMarkets } from "../utils";
import { DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config";
import { setExchangeRate, waitRegisteredLendingMarkets } from "../utils";
import { startLiquidator } from "../../src";
import { APPROX_BLOCK_TIME_MS } from "../../src/consts";

describe("liquidate", () => {
const approx10Blocks = 10 * APPROX_BLOCK_TIME_MS;
Expand Down Expand Up @@ -33,7 +35,6 @@ describe("liquidate", () => {

before(async function () {
api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT);
await waitForNthBlock(api);
keyring = new Keyring({ type: "sr25519" });
userAccount = keyring.addFromUri(DEFAULT_USER_1_URI);
user2Account = keyring.addFromUri(DEFAULT_USER_2_URI);
Expand Down Expand Up @@ -141,13 +142,14 @@ describe("liquidate", () => {
await userInterBtcAPI.loans.borrow(borrowAmount1.currency, borrowAmount1);
await userInterBtcAPI.loans.borrow(borrowAmount2.currency, borrowAmount2);

// start liquidation listener
// lendingLiquidationChecker(user2InterBtcAPI)
// TODO!: start the bot listener logic
// Start liquidation listener
// Do not `await` so it runs in the background
startLiquidator(sudoInterBtcAPI);

const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks);

// crash the collateral exchange rate
const newExchangeRate = "0x00000000000000000001000000000000";
const newExchangeRate = "0x00000000000000000000001000000000";
await setExchangeRate(sudoInterBtcAPI, depositAmount.currency, newExchangeRate);

// expect liquidation event to happen
Expand Down
108 changes: 108 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
version: "3.8"
services:
interbtc:
image: "interlayhq/interbtc:1.21.10"
command:
- --rpc-external
- --ws-external
- --rpc-methods=unsafe
- --dev
- --instant-seal
ports:
- "9933:9933"
- "9944:9944"
bitcoind:
image: "ruimarinho/bitcoin-core:22"
command:
- -regtest
- -server
- -rpcbind=0.0.0.0
- -rpcallowip=0.0.0.0/0
- -rpcuser=rpcuser
- -rpcpassword=rpcpassword
- -fallbackfee=0.0002
ports:
- "18443:18443"
bitcoin-cli:
image: "ruimarinho/bitcoin-core:22"
command:
- /bin/sh
- -c
- |
bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword createwallet Alice
ALICE_ADDRESS=$$(bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword -rpcwallet=Alice getnewaddress)
# coins need 100 confirmations to be spendable
bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword generatetoaddress 101 $${ALICE_ADDRESS}
electrs:
image: "interlayhq/electrs:latest"
command:
- electrs
- -vvvv
- --network
- regtest
- --jsonrpc-import
- --cors
- "*"
- --cookie
- "rpcuser:rpcpassword"
- --daemon-rpc-addr
- "bitcoind:18443"
- --http-addr
- "[::0]:3002"
- --index-unspendables
ports:
- "3002:3002"
restart: always
oracle:
image: "interlayhq/interbtc-clients:oracle-parachain-metadata-kintsugi-testnet-1.19.7"
command:
- oracle-parachain-metadata-kintsugi-testnet
- --keyring=bob
- --btc-parachain-url=ws://interbtc:9944
environment:
RUST_LOG: info
volumes:
- ./docker/oracle-config.json:/oracle-config.json
vault_1:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7"
command:
- vault-parachain-metadata-kintsugi-testnet
- --keyfile=/keyfile.json
- --keyname=vault_1
- --auto-register=KSM=10000000000000
- --auto-register=KINT=180000000000000
- --btc-parachain-url=ws://interbtc:9944
- --bitcoin-relay-start-height=1
environment: &client-env
BITCOIN_RPC_URL: http://bitcoind:18443
BITCOIN_RPC_USER: rpcuser
BITCOIN_RPC_PASS: rpcpassword
RUST_LOG: info
volumes:
- ./docker/vault_1-keyfile.json:/keyfile.json
vault_2:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7"
command:
- vault-parachain-metadata-kintsugi-testnet
- --keyfile=/keyfile.json
- --keyname=vault_2
- --auto-register=KSM=10000000000000
- --auto-register=KINT=180000000000000
- --btc-parachain-url=ws://interbtc:9944
- --bitcoin-relay-start-height=1
environment: *client-env
volumes:
- ./docker/vault_2-keyfile.json:/keyfile.json
vault_3:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7"
command:
- vault-parachain-metadata-kintsugi-testnet
- --keyfile=/keyfile.json
- --keyname=vault_3
- --auto-register=KSM=10000000000000
- --auto-register=KINT=180000000000000
- --btc-parachain-url=ws://interbtc:9944
- --bitcoin-relay-start-height=1
environment: *client-env
volumes:
- ./docker/vault_3-keyfile.json:/keyfile.json
32 changes: 32 additions & 0 deletions docker/oracle-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"currencies": {
"BTC": {
"name": "Bitcoin",
"decimals": 8
},
"KINT": {
"name": "Kintsugi",
"decimals": 12
},
"KSM": {
"name": "Kusama",
"decimals": 12
}
},
"prices": [
{
"pair": [
"BTC",
"KINT"
],
"value": 230
},
{
"pair": [
"BTC",
"KSM"
],
"value": 230
}
]
}
3 changes: 3 additions & 0 deletions docker/vault_1-keyfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vault_1": "//Charlie//stash"
}
3 changes: 3 additions & 0 deletions docker/vault_2-keyfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vault_2": "//Dave//stash"
}
3 changes: 3 additions & 0 deletions docker/vault_3-keyfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vault_3": "//Eve//stash"
}
12 changes: 0 additions & 12 deletions scripts/docker-setup.sh

This file was deleted.

17 changes: 0 additions & 17 deletions scripts/setup-parachain-docker.ts

This file was deleted.

0 comments on commit fdc8ab9

Please sign in to comment.