Skip to content

Commit

Permalink
Add liquidation bot example script that uses the SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
danielattilasimon committed Feb 11, 2021
1 parent 462059b commit 8459cdf
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 0 deletions.
1 change: 1 addition & 0 deletions filter-repo/1-included-paths
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ docs/
packages/decimal/
packages/frontend/
packages/dev-frontend/
packages/examples/
packages/providers/
packages/subgraph/
packages/lib/
Expand Down
14 changes: 14 additions & 0 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@liquity/examples",
"version": "0.0.1",
"private": true,
"dependencies": {
"@liquity/lib-base": "^0.0.1",
"@liquity/lib-ethers": "^0.0.1",
"chalk": "^4.1.0",
"ethers": "^5.0.0"
},
"scripts": {
"liqbot": "node src/liqbot.js"
}
}
144 changes: 144 additions & 0 deletions packages/examples/src/liqbot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const { red, blue, green, yellow, dim, bold } = require("chalk");

const { JsonRpcProvider } = require("@ethersproject/providers");
const { Wallet } = require("@ethersproject/wallet");

const { Decimal, Trove, LUSD_LIQUIDATION_RESERVE } = require("@liquity/lib-base");
const { EthersLiquity, EthersLiquityWithStore } = require("@liquity/lib-ethers");

function log(message) {
console.log(`${dim(`[${new Date().toLocaleTimeString()}]`)} ${message}`);
}

const info = message => log(`${blue("ℹ")} ${message}`);
const warn = message => log(`${yellow("‼")} ${message}`);
const error = message => log(`${red("✖")} ${message}`);
const success = message => log(`${green("✔")} ${message}`);

async function main() {
// Replace URL if not using a local node
const provider = new JsonRpcProvider("http://localhost:8545");
const wallet = new Wallet(process.env.PRIVATE_KEY).connect(provider);
const liquity = await EthersLiquity.connect(wallet, { useStore: "blockPolled" });

liquity.store.onLoaded = () => {
info("Waiting for price drops...");
tryToLiquidate(liquity);
};

liquity.store.subscribe(({ newState, oldState }) => {
// Try to liquidate whenever the price drops
if (newState.price.lt(oldState.price)) {
tryToLiquidate(liquity);
}
});

liquity.store.start();
}

/**
* @param {Decimal} [price]
* @returns {(trove: [string, Trove]) => boolean}
*/
const underCollateralized = price => ([, trove]) => trove.collateralRatioIsBelowMinimum(price);

/**
* @param {[string, Trove]}
* @param {[string, Trove]}
*/
const byDescendingCollateral = ([, { collateral: a }], [, { collateral: b }]) =>
b.gt(a) ? 1 : b.lt(a) ? -1 : 0;

/**
* @param {[string[], Trove[]]}
* @param {[string, Trove]}
* @returns {[string[], Trove[]]}
*/
const unzip = ([addresses, troves], [address, trove]) => [
addresses.concat(address),
troves.concat(trove)
];

/**
* @param {EthersLiquityWithStore} [liquity]
*/
async function tryToLiquidate(liquity) {
const { store } = liquity;

const [gasPrice, riskiestTroves] = await Promise.all([
liquity.connection.provider
.getGasPrice()
.then(bn => Decimal.fromBigNumberString(bn.toHexString())),

liquity.getTroves({
first: 1000,
sortedBy: "ascendingCollateralRatio"
})
]);

const [addresses, troves] = riskiestTroves
.filter(underCollateralized(store.state.price))
.sort(byDescendingCollateral)
.slice(0, 40)
.reduce(unzip, [[], []]);

if (troves.length === 0) {
// Nothing to liquidate
return;
}

try {
const liquidation = await liquity.populate.liquidate(addresses, { gasPrice: gasPrice.hex });
const gasLimit = liquidation.rawPopulatedTransaction.gasLimit.toNumber();
const expectedCost = gasPrice.mul(gasLimit).mul(store.state.price);

const total = troves.reduce((a, b) => a.add(b));
const expectedCompensation = total.collateral
.mul(0.005)
.mul(store.state.price)
.add(LUSD_LIQUIDATION_RESERVE.mul(troves.length));

if (expectedCost.gt(expectedCompensation)) {
// In reality, the TX cost will be lower than this thanks to storage refunds, but let's be
// on the safe side.
warn(
"Skipping liquidation due to high TX cost " +
`($${expectedCost.toString(2)} > $${expectedCompensation.toString(2)}).`
);
return;
}

info(`Attempting to liquidate ${troves.length} Trove(s)...`);

const tx = await liquidation.send();
const receipt = await tx.waitForReceipt();

if (receipt.status === "failed") {
error(`TX ${receipt.rawReceipt.transactionHash} failed.`);
return;
}

const { collateralGasCompensation, lusdGasCompensation, liquidatedAddresses } = receipt.details;
const gasCost = gasPrice.mul(receipt.rawReceipt.gasUsed.toNumber()).mul(store.state.price);
const totalCompensation = collateralGasCompensation
.mul(store.state.price)
.add(lusdGasCompensation);

success(
`Received ${bold(`${collateralGasCompensation.toString(4)} ETH`)} + ` +
`${bold(`${lusdGasCompensation.toString(2)} LUSD`)} compensation (` +
(totalCompensation.gte(gasCost)
? `${green(`$${totalCompensation.sub(gasCost).toString(2)}`)} profit`
: `${red(`$${gasCost.sub(totalCompensation).toString(2)}`)} loss`) +
`) for liquidating ${liquidatedAddresses.length} Trove(s).`
);
} catch (err) {
error("Unexpected error:");
console.error(err);
}
}

main().catch(err => {
console.error(err);
process.exit(1);
});

0 comments on commit 8459cdf

Please sign in to comment.