Skip to content

Commit

Permalink
Merge pull request #8 from morpho-labs/feat/modularize-bot-liquidatio…
Browse files Browse the repository at this point in the history
…n-handler

Feat/modularize bot liquidation handler
  • Loading branch information
julien-devatom authored Dec 1, 2022
2 parents 7a31f52 + 29688ee commit a559822
Show file tree
Hide file tree
Showing 19 changed files with 377 additions and 85 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,29 @@ To run a liquidation check, you just have to set the right environment variables
- `PRIVATE_KEY`: the private key of the account that will be used to send the transactions. If not provided, you'll run the bot in read only mode.
Your address must be an allowed liquidator of the flash liquidator contract. The two example addresses in the `.env.example` file are the ones of Morpho Labs.
- `ALCHEMY_KEY`: the Alchemy key to connect to the Ethereum network.
- `LIQUIDATOR_ADDRESSES`: a comma separated list of the liquidator contract addresses to use.
- `LIQUIDATOR_ADDRESSES`: a comma separated list of the liquidator contract addresses to use. Only used if you use the `--flash` option.
- `PROFITABLE_THRESHOLD`: the liquidation threshold to use (in USD).
- `BATCH_SIZE`: The number of parallel queries sent to the Ethereum network.
- `PROTOCOLS`: The underlying protocols to use (comma separated list).
- `DELAY`: The delay between two liquidations check. If not provided, the bot will run only once.

Then, you can just run:

#### EOA Liquidation
```shell
yarn run:bot
```
Without the `--flash` option, the bot will use your wallet to liquidate without using any contract. In this case, you need to have
the funds to liquidate the user. Else, the script will throw an error.

#### Flash Liquidation
```shell
yarn run:bot --flash
```
In order to use flash liquidation, you have to deploy the contracts first, and then set the `LIQUIDATOR_ADDRESSES` environment variable.

### Remarks
The script can throw an error and stop execution in some case, specially if you have a blockchain provider error, if you have not enough
funds to send any transaction, or if you have not enough funds to liquidate the user with EOA mode.

### Remotely

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"license": "MIT",
"scripts": {
"test": "yarn hardhat test",
"run:bot": "yarn build:bot && hardhat run scripts/runBot.ts",
"run:bot": "yarn build:bot && ts-node scripts/runBot.ts",
"lint:fix": "npx prettier '**/*.{json,sol,md,ts}' --write",
"deploy:contracts": "hardhat run scripts/deploy.ts --network mainnet",
"compile": "hardhat compile",
Expand Down
58 changes: 44 additions & 14 deletions scripts/runBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,30 @@ import LiquidationBot from "../src/LiquidationBot";
import ConsoleLog from "../src/loggers/ConsoleLog";
import { ILiquidator__factory } from "../typechain";
import { AVAILABLE_PROTOCOLS } from "../config";
import { ILiquidationHandler } from "../src/LiquidationHandler/LiquidationHandler.interface";
import LiquidatorHandler from "../src/LiquidationHandler/LiquidatorHandler";
import {
MorphoAaveV2,
MorphoCompound,
} from "@morpho-labs/morpho-ethers-contract";
import ReadOnlyHandler from "../src/LiquidationHandler/ReadOnlyHandler";
import EOAHandler from "../src/LiquidationHandler/EOAHandler";

dotenv.config();

const initializers: Record<
string,
(
provider: providers.Provider
) => Promise<{ fetcher: IFetcher; adapter: IMorphoAdapter }>
(provider: providers.Provider) => Promise<{
fetcher: IFetcher;
adapter: IMorphoAdapter;
morpho: MorphoCompound | MorphoAaveV2;
}>
> = {
aave: initAave,
compound: initCompound,
};
const main = async (): Promise<any> => {
const useFlashLiquidator = process.argv.includes("--flash");
const pk = process.env.PRIVATE_KEY;
const provider = new providers.AlchemyProvider(1, process.env.ALCHEMY_KEY);

Expand All @@ -33,12 +44,15 @@ const main = async (): Promise<any> => {
}

// Check liquidator addresses

const liquidatorAddresses = process.env.LIQUIDATOR_ADDRESSES?.split(",");
if (!liquidatorAddresses) throw new Error("No liquidator addresses found");
liquidatorAddresses.forEach((liquidatorAddress) => {
if (!isAddress(liquidatorAddress))
throw new Error(`Invalid liquidator address ${liquidatorAddress}`);
});
if (useFlashLiquidator) {
if (!liquidatorAddresses) throw new Error("No liquidator addresses found");
liquidatorAddresses.forEach((liquidatorAddress) => {
if (!isAddress(liquidatorAddress))
throw new Error(`Invalid liquidator address ${liquidatorAddress}`);
});
}

// Check protocols
const protocols = process.env.PROTOCOLS?.split(",");
Expand All @@ -47,22 +61,38 @@ const main = async (): Promise<any> => {
if (!AVAILABLE_PROTOCOLS.includes(protocol))
throw new Error(`Invalid protocol ${protocol}`);
});
if (protocols.length !== liquidatorAddresses.length)
if (useFlashLiquidator && protocols.length !== liquidatorAddresses!.length)
throw new Error(
"Number of protocols and liquidator addresses must be the same"
);
const logger = new ConsoleLog();

for (let i = 0; i < protocols.length; i++) {
const protocol = protocols[i];
const liquidatorAddress = liquidatorAddresses[i];
const { adapter, fetcher, morpho } = await initializers[protocol](provider);

let liquidationHandler: ILiquidationHandler;
if (useFlashLiquidator) {
liquidationHandler = new LiquidatorHandler(
ILiquidator__factory.connect(liquidatorAddresses![i], provider as any),
wallet!,
logger
);
console.log("Using flash liquidator");
} else if (!wallet) {
liquidationHandler = new ReadOnlyHandler(logger);
console.log("Using read only handler");
} else {
liquidationHandler = new EOAHandler(morpho, wallet, logger);
console.log("Using EOA handler");
}
console.time(protocol);
console.timeLog(protocol, `Starting bot initialization`);
const { adapter, fetcher } = await initializers[protocol](provider);
const bot = new LiquidationBot(
new ConsoleLog(),
logger,
fetcher,
wallet,
ILiquidator__factory.connect(liquidatorAddress, provider as any),
provider,
liquidationHandler,
adapter,
{
profitableThresholdUSD: parseUnits(
Expand Down
104 changes: 59 additions & 45 deletions src/LiquidationBot.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { BigNumber, getDefaultProvider, providers, Signer } from "ethers";
import { BigNumber, providers } from "ethers";
import { Logger } from "./interfaces/logger";
import { IFetcher } from "./interfaces/IFetcher";
import { formatUnits, parseUnits } from "ethers/lib/utils";
import { pow10 } from "../test/helpers";
import stablecoins from "./constant/stablecoins";
import { ethers } from "hardhat";
import config from "../config";
import underlyings from "./constant/underlyings";
import { getPoolData, UniswapPool } from "./uniswap/pools";
import { IMorphoAdapter } from "./morpho/Morpho.interface";
import { ILiquidator } from "../typechain";
import {
ILiquidationHandler,
LiquidationParams,
} from "./LiquidationHandler/LiquidationHandler.interface";
import { PercentMath } from "@morpho-labs/ethers-utils/lib/maths";

export interface LiquidationBotSettings {
profitableThresholdUSD: BigNumber;
Expand All @@ -26,21 +29,14 @@ export default class LiquidationBot {
constructor(
public readonly logger: Logger,
public readonly fetcher: IFetcher,
public readonly signer: Signer | undefined,
public readonly liquidator: ILiquidator,
public readonly provider: providers.Provider,
public readonly liquidationHandler: ILiquidationHandler,
public readonly adapter: IMorphoAdapter,
settings: Partial<LiquidationBotSettings> = {}
) {
this.settings = { ...defaultSettings, ...settings };
}

get provider() {
if (this.signer?.provider) return this.signer.provider;
if (process.env.ALCHEMY_KEY)
return new providers.AlchemyProvider("1", process.env.ALCHEMY_KEY);
return getDefaultProvider();
}

async computeLiquidableUsers() {
let lastId = "";
let hasMore = true;
Expand Down Expand Up @@ -68,6 +64,24 @@ export default class LiquidationBot {
return liquidableUsers;
}

async liquidate(
poolTokenBorrowed: string,
poolTokenCollateral: string,
user: string,
amount: BigNumber,
swapPath: string
) {
const liquidationParams: LiquidationParams = {
poolTokenBorrowed,
poolTokenCollateral,
underlyingBorrowed: underlyings[poolTokenBorrowed.toLowerCase()],
user,
amount,
swapPath,
};
return this.liquidationHandler.handleLiquidation(liquidationParams);
}

async getUserLiquidationParams(userAddress: string) {
// first fetch all user balances
const markets = await this.adapter.getMarkets();
Expand Down Expand Up @@ -181,26 +195,13 @@ export default class LiquidationBot {
);
}

isProfitable(toLiquidate: BigNumber, price: BigNumber) {
return toLiquidate
.mul(price)
.div(pow10(18))
.mul(7)
.div(100)
.gt(this.settings.profitableThresholdUSD);
}

async liquidate(...args: any) {
if (!this.signer) return;
const tx = await this.liquidator
.connect(this.signer)
// @ts-ignore
.liquidate(...args, { gasLimit: 8_000_000 })
.catch(this.logError.bind(this));
if (!tx) return;
this.logger.log(tx);
const receipt = await tx.wait().catch(this.logError.bind(this));
if (receipt) this.logger.log(`Gas used: ${receipt.gasUsed.toString()}`);
async isProfitable(market: string, toLiquidate: BigNumber, price: BigNumber) {
const rewards = await this.adapter.getLiquidationBonus(market);
const usdAmount = await this.adapter.toUsd(market, toLiquidate, price);
return PercentMath.percentMul(
usdAmount,
rewards.sub(PercentMath.BASE_PERCENT)
).gt(this.settings.profitableThresholdUSD);
}

async checkPoolLiquidity(borrowMarket: string, collateralMarket: string) {
Expand Down Expand Up @@ -256,24 +257,37 @@ export default class LiquidationBot {
const liquidationsParams = await Promise.all(
users.map((u) => this.getUserLiquidationParams(u.address))
);
const toLiquidate = liquidationsParams.filter((user) =>
this.isProfitable(user.toLiquidate, user.debtMarket.price)
);
const toLiquidate = (
await Promise.all(
liquidationsParams.map(async (user) => {
if (
await this.isProfitable(
user.debtMarket.market,
user.toLiquidate,
user.debtMarket.price
)
)
return user;
return null;
})
)
).filter(Boolean);
if (toLiquidate.length > 0) {
this.logger.log(`${toLiquidate.length} users to liquidate`);
for (const userToLiquidate of toLiquidate) {
const swapPath = this.getPath(
userToLiquidate.debtMarket.market,
userToLiquidate.collateralMarket.market
);
await this.liquidate(
userToLiquidate.debtMarket.market,
userToLiquidate.collateralMarket.market,
userToLiquidate.userAddress,
userToLiquidate.toLiquidate,
true,
swapPath
userToLiquidate!.debtMarket.market,
userToLiquidate!.collateralMarket.market
);
const liquidateParams: LiquidationParams = {
poolTokenBorrowed: userToLiquidate!.debtMarket.market,
poolTokenCollateral: userToLiquidate!.collateralMarket.market,
underlyingBorrowed: underlyings[userToLiquidate!.debtMarket.market],
user: userToLiquidate!.userAddress,
amount: userToLiquidate!.toLiquidate,
swapPath,
};
await this.liquidationHandler.handleLiquidation(liquidateParams);
}
}
}
Expand Down
Loading

0 comments on commit a559822

Please sign in to comment.