├── .changeset ├── cargo.js ├── commit.js └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ ├── build.yaml │ ├── deploy.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── .prettierrc.yml ├── .solhint.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── contracts └── Liquidator.sol ├── deploy └── Liquidator.ts ├── deployments ├── goerli │ ├── .chainId │ ├── Liquidator.json │ ├── UniswapV3Factory.json │ └── UniswapV3Router.json ├── mainnet │ ├── .chainId │ ├── Liquidator.json │ ├── UniswapV3Factory.json │ └── UniswapV3Router.json └── optimism │ ├── .chainId │ ├── Liquidator.json │ ├── UniswapV3Factory.json │ └── UniswapV3Router.json ├── foundry.toml ├── funding.json ├── hardhat.config.ts ├── lib └── abi │ ├── Lido.json │ └── LidoOracle.json ├── package-lock.json ├── package.json ├── remappings.txt ├── requirements.txt ├── slither.config.json ├── src ├── account.rs ├── config.rs ├── exactly_events.rs ├── fixed_point_math.rs ├── generate_abi.rs ├── liquidation.rs ├── main.rs ├── market.rs ├── network.rs └── protocol.rs ├── test └── Liquidator.t.sol └── tsconfig.json /.changeset/cargo.js: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | const { readFileSync, writeFileSync } = require("fs"); 3 | const { parse, stringify, basic } = require("@ltd/j-toml"); 4 | const { version } = require("../package.json"); 5 | const cargo = parse(readFileSync("Cargo.toml"), { x: { literal: true } }); 6 | cargo.package.version = basic(version); 7 | writeFileSync("Cargo.toml", stringify(cargo, { newline: "\n", newlineAround: "section" }).trimStart()); 8 | exec("cargo metadata"); 9 | -------------------------------------------------------------------------------- /.changeset/commit.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@changesets/types').CommitFunctions} */ 2 | module.exports = { 3 | getVersionMessage: ({ releases: [{ newVersion }] }) => Promise.resolve(`🔖 v${newVersion}`), 4 | }; 5 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "baseBranch": "main", 5 | "access": "public", 6 | "privatePackages": { "version": true, "tag": true }, 7 | "commit": "./commit.js" 8 | } 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | /src/ 4 | /types/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { project: "./tsconfig.json" }, 5 | settings: { "import/resolver": "typescript" }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:node/recommended", 11 | "plugin:prettier/recommended", 12 | "plugin:eslint-comments/recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | ], 15 | rules: { 16 | "no-console": "error", 17 | "node/no-missing-import": "off", 18 | "node/no-unpublished-import": "off", 19 | "@typescript-eslint/no-shadow": "error", 20 | "eslint-comments/no-unused-disable": "error", 21 | "@typescript-eslint/no-floating-promises": "error", 22 | "import/no-extraneous-dependencies": ["error", { devDependencies: true }], 23 | "@typescript-eslint/no-unused-vars": ["error", { ignoreRestSiblings: true }], 24 | "node/no-unsupported-features/es-syntax": ["error", { ignores: ["modules"] }], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bronzelle 2 | /contracts/ @cruzdanilo @santichez 3 | /deploy/ @cruzdanilo 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | DOCKER_BUILDKIT: 1 12 | PROJECT_ID: exactly-liq-bot 13 | IMAGE_TAG: ${{ format('gcr.io/exactly-liq-bot/liquidation-bot:{0}', github.event.pull_request.head.sha || github.sha) }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | cache: npm 19 | - run: npm ci --omit=dev --legacy-peer-deps 20 | - run: docker build -t ${{ env.IMAGE_TAG }} . 21 | - uses: google-github-actions/auth@v1 22 | with: 23 | credentials_json: ${{ secrets.SERVICE_ACCOUNT_KEY }} 24 | - uses: google-github-actions/setup-gcloud@v1 25 | with: 26 | project_id: ${{ env.PROJECT_ID }} 27 | - run: gcloud auth configure-docker 28 | - run: docker push ${{ env.IMAGE_TAG }} 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_run: 3 | workflows: [.github/workflows/build.yaml] 4 | types: [completed] 5 | workflow_dispatch: 6 | inputs: 7 | environment: 8 | description: 'environment to deploy' 9 | required: true 10 | type: environment 11 | jobs: 12 | deploy: 13 | environment: ${{ inputs.environment || 'goerli' }} 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} 16 | env: 17 | IMAGE_TAG: ${{ format('gcr.io/exactly-liq-bot/liquidation-bot:{0}', github.sha) }} 18 | steps: 19 | - uses: google-github-actions/auth@v1 20 | with: 21 | credentials_json: ${{ secrets.SERVICE_ACCOUNT_KEY }} 22 | - uses: google-github-actions/get-gke-credentials@v1 23 | with: 24 | cluster_name: exactly 25 | location: us-central1 26 | - run: kubectl set image deployment/${{ inputs.environment || 'goerli' }} liquidation-bot=${{ env.IMAGE_TAG }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | token: ${{ secrets.PR_GITHUB_TOKEN }} 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | cache: npm 19 | - run: npm ci --ignore-scripts --legacy-peer-deps 20 | - uses: crazy-max/ghaction-import-gpg@v5 21 | with: 22 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 23 | git_user_signingkey: true 24 | git_commit_gpgsign: true 25 | - uses: changesets/action@v1 26 | with: 27 | title: 🔖 new release 28 | version: npm run version 29 | publish: npx changeset publish 30 | setupGitUser: false 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.PR_GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /artifacts/ 2 | /coverage/ 3 | /coverage.json 4 | /deployments/localhost/ 5 | /deployments/*/solcInputs/ 6 | /docs/ 7 | /target/ 8 | /types/ 9 | 10 | .vscode/ 11 | .cache*/ 12 | cache/ 13 | node_modules/ 14 | .DS_Store 15 | .env 16 | .env.* 17 | *.log 18 | *.code-workspace 19 | .*-version* 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | compiler: 0.8.17 2 | tabWidth: 2 3 | printWidth: 120 4 | trailingComma: all 5 | explicitTypes: always 6 | bracketSpacing: true 7 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "max-line-length": ["error", 120], 6 | "compiler-version": ["error", "0.8.17"], 7 | "func-visibility": ["error", { "ignoreConstructors": true }], 8 | "func-param-name-mixedcase": "error", 9 | "modifier-name-mixedcase": "error", 10 | "constructor-syntax": "error", 11 | "no-inline-assembly": "off", 12 | "not-rely-on-time": "off", 13 | "reason-string": "off", 14 | "prettier/prettier": "error" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @exactly/liquidation-bot 2 | 3 | ## 0.1.20 4 | 5 | ### Patch Changes 6 | 7 | - a0e06ae: Add PAGE_SIZE as environment variable 8 | - fc714cb: ✨ refactor cache recovery and price updates 9 | - 8734902: 🐛 fix validation for unprofitable liquidations 10 | 11 | ## 0.1.19 12 | 13 | ### Patch Changes 14 | 15 | - f50d9bf: ⬆️ upgrade protocol to `v0.2.10` 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "liq-bot" 3 | version = "0.1.19" 4 | license = "MIT" 5 | authors = [ 6 | "Rodrigo Bronzelle ", 7 | "danilo neves cruz ", 8 | ] 9 | edition = "2021" 10 | default-run = "liq-bot" 11 | 12 | [features] 13 | liquidation-stats = [] 14 | complete-compare = [] 15 | 16 | [dependencies] 17 | cacache = "12.0.0" 18 | dotenv = "0.15.0" 19 | ethers = { version = "2.0.10", features = ["ws", "rustls"] } 20 | eyre = "0.6.8" 21 | futures = "0.3.28" 22 | hex = "0.4.3" 23 | log = { version = "0.4.20", features = ["std"] } 24 | pin-project-lite = "0.2.13" 25 | pretty_env_logger = "0.5.0" 26 | reqwest = { version = "0.11.22", features = ["json", "blocking"] } 27 | sentry = { version = "0.31.7", features = ["log", "debug-images"] } 28 | serde = { version = "1.0.189", features = ["derive"] } 29 | serde_json = "1.0.107" 30 | tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } 31 | tokio-stream = "0.1.14" 32 | url = "2.4.1" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.2 2 | FROM rustlang/rust:nightly-bookworm-slim AS builder 3 | 4 | # hadolint ignore=DL3008 5 | RUN apt-get update && apt-get install --no-install-recommends -y pkg-config libssl-dev 6 | 7 | WORKDIR /liq-bot 8 | 9 | COPY node_modules node_modules 10 | COPY Cargo.lock . 11 | COPY Cargo.toml . 12 | COPY deployments deployments 13 | COPY src src 14 | COPY lib lib 15 | 16 | RUN rustup component add rustfmt clippy 17 | 18 | RUN cargo fmt --check \ 19 | && cargo clippy -- -D warnings \ 20 | && cargo build --release 21 | 22 | FROM debian:bookworm-slim 23 | 24 | # hadolint ignore=DL3008 25 | RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates \ 26 | && apt-get clean \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | WORKDIR /liq-bot 30 | 31 | COPY --from=builder /liq-bot/target/release/liq-bot . 32 | COPY --from=builder /liq-bot/deployments deployments 33 | COPY --from=builder /liq-bot/node_modules/@exactly/protocol/deployments node_modules/@exactly/protocol/deployments 34 | 35 | ENTRYPOINT [ "/liq-bot/liq-bot" ] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Exafin Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquidation bot 2 | 3 | ## Dependencies 4 | 5 | - Rust; 6 | - Node. 7 | 8 | The liquidation bot is written in Rust; therefore, it must be installed and set up on the machine. 9 | 10 | ## How to install and run it 11 | 12 | Cloning the project: 13 | 14 | ```shell 15 | git clone git@github.com:exactly-protocol/liquidation-bot.git 16 | ``` 17 | 18 | Deploying flash loan contracts: 19 | 20 | ```shell 21 | npm i --legacy-peer-deps 22 | npx hardhat --network deploy 23 | ``` 24 | 25 | Running the project: 26 | 27 | A `.env` file should be created on the root directory of the project with the following parameters: 28 | 29 | ```env 30 | CHAIN_ID=[ID of the chain used] 31 | [CHAIN_NAME]_NODE=[Link to the RPC provider - MUST be a WebSocket address] 32 | [CHAIN_NAME]_NODE_RELAYER=[Link to the RPC provider used on liquidations - MUST be an HTTP address] 33 | MNEMONIC=[Seed phrase for the bot's account] 34 | REPAY_OFFSET=1 35 | TOKEN_PAIRS=[Pairs of tokens with their fees on Uniswap - it should follows this format `[["TOKEN1","TOKEN2", FEE12],["TOKEN3","TOKEN4", FEE34]...]`] 36 | BACKUP=0 37 | ``` 38 | 39 | After the `.env` file has been created, run the project with the command: 40 | 41 | ```shell 42 | cargo run 43 | ``` 44 | 45 | ## How it works 46 | 47 | ### The Bot 48 | 49 | The bot works by remounting the users' positions using the protocol's emitted events with the minimum number of calls directly to the contracts. This makes the bot more efficient in recreating such states. 50 | 51 | After the bot connects to the RPC provider through WebSocket, it subscribes to receive the events stream. 52 | 53 | Each one of those events is parsed and transcribed into the user's data. 54 | 55 | Whenever there's an idle moment on receiving new events, the bot does a check for liquidations. 56 | 57 | If a user is in a state to be liquidated (with a health factor smaller than 1), the flash loan contract's liquidation function is called. 58 | 59 | The debt is liquidated. 60 | 61 | The bot liquidates the user's debt seizing their collateral with the highest value. 62 | 63 | All the users that could be liquidated will be. 64 | After the liquidations, the bot returns to wait for more events and recreate the user's positions. 65 | 66 | ### Flash loan contract 67 | 68 | The flash loan contract calls protocol's liquidation function. 69 | 70 | It checks its amount on the specific debt asset available on the contract's balance to repay the user's debt. In case it has less than the amount needed to liquidate the user, it does as follow: 71 | 72 | 1. Borrow on Uniswap V3 the difference between what it has and the user's debt; 73 | 1. Waits for a callback notifying it that the amount was received; 74 | 1. Repays the debt; 75 | 1. Receives the collateral; 76 | 1. Swaps it to the same as the user's debt; 77 | 1. Repays Uniswap. 78 | 79 | ## Structure 80 | 81 | The project is structured as follows: 82 | 83 | - main.rs 84 | 85 | Set up the bot to connect correctly to RPC Provider. 86 | 87 | Starts the service and handles most of the errors. 88 | 89 | - protocol.rs 90 | 91 | It's where most of the tasks are executed. 92 | 93 | - A subscription to the event's stream is made on the main thread; 94 | 95 | - Each event received is parsed; 96 | 97 | - Positions are created; 98 | 99 | - A debounce for idleness is made in another thread; 100 | 101 | - When the bot is idle for enough time, this thread checks for liquidations 102 | and send them to the `liquidation` module; 103 | 104 | - liquidation.rs 105 | 106 | Carry a queue of liquidations to be executed. The health factor is re-calculated before the `liquidate` function is called. 107 | 108 | - account.rs 109 | 110 | This structure is used to store users' data. 111 | 112 | - market.rs 113 | 114 | Stores updated information created by the protocol's events about all the markets. 115 | 116 | - exactly_events.rs 117 | 118 | Redirect the events to suitable structures. 119 | 120 | - config.rs 121 | 122 | Handle environment variables such as RPC provider link access, wallet's mnemonic, etc. 123 | -------------------------------------------------------------------------------- /contracts/Liquidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | pragma solidity 0.8.17; 3 | 4 | import { ERC20 } from "solmate/src/tokens/ERC20.sol"; 5 | import { Auth, Authority } from "solmate/src/auth/Auth.sol"; 6 | import { SafeTransferLib } from "solmate/src/utils/SafeTransferLib.sol"; 7 | import { IUniswapV3FlashCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol"; 8 | import { IUniswapV3SwapCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 9 | import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 10 | import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; 11 | 12 | contract Liquidator is Auth, Authority, IUniswapV3FlashCallback, IUniswapV3SwapCallback { 13 | using SafeTransferLib for ERC20; 14 | 15 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 16 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 17 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 18 | uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; 19 | 20 | address public immutable factory; 21 | ISwapRouter public immutable swapRouter; 22 | 23 | mapping(address => bool) public callers; 24 | 25 | constructor(address owner_, address factory_, ISwapRouter swapRouter_) Auth(owner_, this) { 26 | factory = factory_; 27 | swapRouter = swapRouter_; 28 | } 29 | 30 | function liquidate( 31 | IMarket repayMarket, 32 | IMarket seizeMarket, 33 | address borrower, 34 | uint256 maxRepay, 35 | address poolPair, 36 | uint24 fee, 37 | uint24 pairFee 38 | ) external requiresAuth { 39 | ERC20 repayAsset = repayMarket.asset(); 40 | uint256 availableRepay = repayAsset.balanceOf(address(this)); 41 | 42 | if (availableRepay >= maxRepay) { 43 | repayAsset.safeApprove(address(repayMarket), maxRepay); 44 | repayMarket.liquidate(borrower, maxRepay, seizeMarket); 45 | } else { 46 | uint256 flashBorrow = maxRepay - availableRepay; 47 | if (repayMarket != seizeMarket) { 48 | PoolAddress.PoolKey memory poolKey; 49 | bytes memory data; 50 | if (poolPair == address(0)) { 51 | ERC20 seizeAsset = seizeMarket.asset(); 52 | poolKey = PoolAddress.getPoolKey(address(repayAsset), address(seizeAsset), fee); 53 | data = abi.encode( 54 | SwapCallbackData({ 55 | repayMarket: repayMarket, 56 | seizeMarket: seizeMarket, 57 | borrower: borrower, 58 | poolPair: address(seizeAsset), 59 | fee: fee, 60 | pairFee: 0 61 | }) 62 | ); 63 | } else { 64 | poolKey = PoolAddress.getPoolKey(address(repayAsset), poolPair, fee); 65 | data = abi.encode( 66 | SwapCallbackData({ 67 | repayMarket: repayMarket, 68 | seizeMarket: seizeMarket, 69 | borrower: borrower, 70 | poolPair: poolPair, 71 | fee: fee, 72 | pairFee: pairFee 73 | }) 74 | ); 75 | } 76 | IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)).swap( 77 | address(this), 78 | address(repayAsset) == poolKey.token1, 79 | -int256(maxRepay), 80 | address(repayAsset) == poolKey.token1 ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, 81 | data 82 | ); 83 | } else { 84 | PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(address(repayAsset), poolPair, fee); 85 | bytes memory data = abi.encode( 86 | FlashCallbackData({ 87 | repayMarket: repayMarket, 88 | seizeMarket: seizeMarket, 89 | borrower: borrower, 90 | maxRepay: maxRepay, 91 | flashBorrow: flashBorrow, 92 | poolPair: poolPair, 93 | fee: fee 94 | }) 95 | ); 96 | IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)).flash( 97 | address(this), 98 | address(repayAsset) == poolKey.token0 ? flashBorrow : 0, 99 | address(repayAsset) == poolKey.token1 ? flashBorrow : 0, 100 | data 101 | ); 102 | } 103 | } 104 | } 105 | 106 | // slither-disable-next-line similar-names 107 | function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { 108 | SwapCallbackData memory s = abi.decode(data, (SwapCallbackData)); 109 | ERC20 seizeAsset = s.seizeMarket.asset(); 110 | if (s.borrower != address(0)) { 111 | ERC20 repayAsset = s.repayMarket.asset(); 112 | PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(address(repayAsset), s.poolPair, s.fee); 113 | require(msg.sender == PoolAddress.computeAddress(factory, poolKey)); 114 | 115 | uint256 maxRepay = uint256(-(address(repayAsset) == poolKey.token0 ? amount0Delta : amount1Delta)); 116 | repayAsset.safeApprove(address(s.repayMarket), maxRepay); 117 | s.repayMarket.liquidate(s.borrower, maxRepay, s.seizeMarket); 118 | if (s.pairFee > 0) { 119 | PoolAddress.PoolKey memory swapPoolKey = PoolAddress.getPoolKey(address(seizeAsset), s.poolPair, s.pairFee); 120 | IUniswapV3Pool(PoolAddress.computeAddress(factory, swapPoolKey)).swap( 121 | address(this), 122 | address(seizeAsset) == swapPoolKey.token0, 123 | -int256(s.poolPair == poolKey.token0 ? amount0Delta : amount1Delta), 124 | address(seizeAsset) == swapPoolKey.token0 ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, 125 | abi.encode( 126 | SwapCallbackData({ 127 | repayMarket: IMarket(address(0)), 128 | seizeMarket: s.seizeMarket, 129 | borrower: address(0), 130 | poolPair: s.poolPair, 131 | fee: 0, 132 | pairFee: s.pairFee 133 | }) 134 | ) 135 | ); 136 | 137 | ERC20(s.poolPair).safeTransfer( 138 | msg.sender, 139 | uint256(address(s.poolPair) == poolKey.token0 ? amount0Delta : amount1Delta) 140 | ); 141 | } else { 142 | seizeAsset.safeTransfer( 143 | msg.sender, 144 | uint256(address(seizeAsset) == poolKey.token0 ? amount0Delta : amount1Delta) 145 | ); 146 | } 147 | } else { 148 | PoolAddress.PoolKey memory swapPoolKey = PoolAddress.getPoolKey(address(seizeAsset), s.poolPair, s.pairFee); 149 | require(msg.sender == PoolAddress.computeAddress(factory, swapPoolKey)); 150 | 151 | seizeAsset.safeTransfer( 152 | msg.sender, 153 | uint256(address(seizeAsset) == swapPoolKey.token0 ? amount0Delta : amount1Delta) 154 | ); 155 | } 156 | } 157 | 158 | function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external { 159 | FlashCallbackData memory f = abi.decode(data, (FlashCallbackData)); 160 | ERC20 repayAsset = f.repayMarket.asset(); 161 | PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(address(repayAsset), f.poolPair, f.fee); 162 | 163 | require(msg.sender == PoolAddress.computeAddress(factory, poolKey)); 164 | 165 | repayAsset.safeApprove(address(f.repayMarket), f.maxRepay); 166 | f.repayMarket.liquidate(f.borrower, f.maxRepay, f.seizeMarket); 167 | 168 | repayAsset.safeTransfer(msg.sender, f.flashBorrow + (address(repayAsset) == poolKey.token0 ? fee0 : fee1)); 169 | } 170 | 171 | function swap( 172 | ERC20 assetIn, 173 | uint256 amountIn, 174 | ERC20 assetOut, 175 | uint256 amountOutMinimum, 176 | uint24 fee 177 | ) external requiresAuth { 178 | assetIn.safeApprove(address(swapRouter), amountIn); 179 | swapRouter.exactInputSingle( 180 | ISwapRouter.ExactInputSingleParams({ 181 | tokenIn: address(assetIn), 182 | tokenOut: address(assetOut), 183 | fee: fee, 184 | recipient: address(this), 185 | deadline: block.timestamp, 186 | amountIn: amountIn, 187 | amountOutMinimum: amountOutMinimum, 188 | sqrtPriceLimitX96: 0 189 | }) 190 | ); 191 | } 192 | 193 | function transfer(ERC20 asset, address to, uint256 amount) external requiresAuth { 194 | asset.safeTransfer(to, amount); 195 | } 196 | 197 | function canCall(address caller, address, bytes4 functionSig) external view returns (bool) { 198 | return functionSig == this.liquidate.selector && callers[caller]; 199 | } 200 | 201 | function addCaller(address caller) external requiresAuth { 202 | callers[caller] = true; 203 | } 204 | 205 | function removeCaller(address caller) external requiresAuth { 206 | delete callers[caller]; 207 | } 208 | } 209 | 210 | struct SwapCallbackData { 211 | IMarket repayMarket; 212 | IMarket seizeMarket; 213 | address borrower; 214 | address poolPair; 215 | uint24 fee; 216 | uint24 pairFee; 217 | } 218 | 219 | struct FlashCallbackData { 220 | IMarket repayMarket; 221 | IMarket seizeMarket; 222 | address borrower; 223 | uint256 maxRepay; 224 | uint256 flashBorrow; 225 | address poolPair; 226 | uint24 fee; 227 | } 228 | 229 | interface IMarket { 230 | function asset() external view returns (ERC20); 231 | 232 | function liquidate(address borrower, uint256 maxAssets, IMarket seizeMarket) external returns (uint256 repaidAssets); 233 | } 234 | 235 | // https://github.com/Uniswap/v3-periphery/pull/289 236 | library PoolAddress { 237 | bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; 238 | 239 | struct PoolKey { 240 | address token0; 241 | address token1; 242 | uint24 fee; 243 | } 244 | 245 | function getPoolKey(address tokenA, address tokenB, uint24 fee) internal pure returns (PoolKey memory) { 246 | if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); 247 | return PoolKey({ token0: tokenA, token1: tokenB, fee: fee }); 248 | } 249 | 250 | function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) { 251 | require(key.token0 < key.token1); 252 | pool = address( 253 | uint160( 254 | uint256( 255 | keccak256( 256 | abi.encodePacked( 257 | hex"ff", 258 | factory, 259 | keccak256(abi.encode(key.token0, key.token1, key.fee)), 260 | POOL_INIT_CODE_HASH 261 | ) 262 | ) 263 | ) 264 | ) 265 | ); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /deploy/Liquidator.ts: -------------------------------------------------------------------------------- 1 | import type { DeployFunction } from "hardhat-deploy/types"; 2 | 3 | const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => { 4 | const { deployer, owner } = await getNamedAccounts(); 5 | await deploy("Liquidator", { 6 | args: [owner, (await get("UniswapV3Factory")).address, (await get("UniswapV3Router")).address], 7 | from: deployer, 8 | log: true, 9 | }); 10 | }; 11 | 12 | func.tags = ["Liquidator"]; 13 | 14 | export default func; 15 | -------------------------------------------------------------------------------- /deployments/goerli/.chainId: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /deployments/goerli/UniswapV3Factory.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x1F98431c8aD98523631AE4a59f267346ea31F984" 3 | } 4 | -------------------------------------------------------------------------------- /deployments/goerli/UniswapV3Router.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xE592427A0AEce92De3Edee1F18E0157C05861564" 3 | } 4 | -------------------------------------------------------------------------------- /deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /deployments/mainnet/UniswapV3Factory.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x1F98431c8aD98523631AE4a59f267346ea31F984" 3 | } 4 | -------------------------------------------------------------------------------- /deployments/mainnet/UniswapV3Router.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xE592427A0AEce92De3Edee1F18E0157C05861564" 3 | } 4 | -------------------------------------------------------------------------------- /deployments/optimism/.chainId: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /deployments/optimism/UniswapV3Factory.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x1F98431c8aD98523631AE4a59f267346ea31F984" 3 | } 4 | -------------------------------------------------------------------------------- /deployments/optimism/UniswapV3Router.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xE592427A0AEce92De3Edee1F18E0157C05861564" 3 | } 4 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc = '0.8.17' 3 | optimizer = true 4 | optimizer_runs = 6_666_666 5 | 6 | src = 'test' 7 | out = 'artifacts/.foundry' 8 | cache_path = 'cache/.foundry' 9 | verbosity = 3 10 | 11 | fs_permissions = [{ access = "read", path = "./" }] 12 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x89216c66f45f82595fddb810c78df46fcee9be72181e9c92b26b0ce31bd6f413" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import "hardhat-deploy"; 3 | import "@typechain/hardhat"; 4 | import "@nomiclabs/hardhat-ethers"; 5 | import { env } from "process"; 6 | import { setup } from "@tenderly/hardhat-tenderly"; 7 | import type { HardhatUserConfig } from "hardhat/types"; 8 | 9 | setup({ automaticVerifications: true }); 10 | 11 | export default { 12 | solidity: { version: "0.8.17", settings: { optimizer: { enabled: true, runs: 6_666_666 } } }, 13 | networks: { 14 | mainnet: { 15 | url: env.MAINNET_NODE ?? "https://mainnet.infura.io/", 16 | ...(env.MNEMONIC && { accounts: { mnemonic: env.MNEMONIC } }), 17 | }, 18 | optimism: { 19 | url: env.OPTIMISM_NODE ?? "https://optimism.infura.io/", 20 | ...(env.MNEMONIC && { accounts: { mnemonic: env.MNEMONIC } }), 21 | }, 22 | goerli: { 23 | url: env.GOERLI_NODE ?? "https://goerli.infura.io/", 24 | ...(env.MNEMONIC && { accounts: { mnemonic: env.MNEMONIC } }), 25 | }, 26 | }, 27 | typechain: { outDir: "types" }, 28 | namedAccounts: { 29 | deployer: { default: 0 }, 30 | owner: { 31 | default: 0, 32 | mainnet: "0x382d89aa156C473Fdb1c9565dF309e80e8fA4437", 33 | optimism: "0x59C41d3629F81ef8Ce554B4eB3446a2b6A129260", 34 | goerli: "0x1801f5EAeAbA3fD02cBF4b7ED1A7b58AD84C0705", 35 | }, 36 | }, 37 | tenderly: { project: "exactly", username: "exactly", privateVerification: true }, 38 | } as HardhatUserConfig; 39 | -------------------------------------------------------------------------------- /lib/abi/Lido.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": false, 4 | "inputs": [], 5 | "name": "resume", 6 | "outputs": [], 7 | "payable": false, 8 | "stateMutability": "nonpayable", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": true, 13 | "inputs": [], 14 | "name": "name", 15 | "outputs": [ 16 | { 17 | "name": "", 18 | "type": "string" 19 | } 20 | ], 21 | "payable": false, 22 | "stateMutability": "pure", 23 | "type": "function" 24 | }, 25 | { 26 | "constant": false, 27 | "inputs": [], 28 | "name": "stop", 29 | "outputs": [], 30 | "payable": false, 31 | "stateMutability": "nonpayable", 32 | "type": "function" 33 | }, 34 | { 35 | "constant": true, 36 | "inputs": [], 37 | "name": "hasInitialized", 38 | "outputs": [ 39 | { 40 | "name": "", 41 | "type": "bool" 42 | } 43 | ], 44 | "payable": false, 45 | "stateMutability": "view", 46 | "type": "function" 47 | }, 48 | { 49 | "constant": false, 50 | "inputs": [ 51 | { 52 | "name": "_spender", 53 | "type": "address" 54 | }, 55 | { 56 | "name": "_amount", 57 | "type": "uint256" 58 | } 59 | ], 60 | "name": "approve", 61 | "outputs": [ 62 | { 63 | "name": "", 64 | "type": "bool" 65 | } 66 | ], 67 | "payable": false, 68 | "stateMutability": "nonpayable", 69 | "type": "function" 70 | }, 71 | { 72 | "constant": true, 73 | "inputs": [], 74 | "name": "STAKING_CONTROL_ROLE", 75 | "outputs": [ 76 | { 77 | "name": "", 78 | "type": "bytes32" 79 | } 80 | ], 81 | "payable": false, 82 | "stateMutability": "view", 83 | "type": "function" 84 | }, 85 | { 86 | "constant": false, 87 | "inputs": [ 88 | { 89 | "name": "_depositContract", 90 | "type": "address" 91 | }, 92 | { 93 | "name": "_oracle", 94 | "type": "address" 95 | }, 96 | { 97 | "name": "_operators", 98 | "type": "address" 99 | }, 100 | { 101 | "name": "_treasury", 102 | "type": "address" 103 | }, 104 | { 105 | "name": "_insuranceFund", 106 | "type": "address" 107 | } 108 | ], 109 | "name": "initialize", 110 | "outputs": [], 111 | "payable": false, 112 | "stateMutability": "nonpayable", 113 | "type": "function" 114 | }, 115 | { 116 | "constant": true, 117 | "inputs": [], 118 | "name": "getInsuranceFund", 119 | "outputs": [ 120 | { 121 | "name": "", 122 | "type": "address" 123 | } 124 | ], 125 | "payable": false, 126 | "stateMutability": "view", 127 | "type": "function" 128 | }, 129 | { 130 | "constant": true, 131 | "inputs": [], 132 | "name": "totalSupply", 133 | "outputs": [ 134 | { 135 | "name": "", 136 | "type": "uint256" 137 | } 138 | ], 139 | "payable": false, 140 | "stateMutability": "view", 141 | "type": "function" 142 | }, 143 | { 144 | "constant": true, 145 | "inputs": [ 146 | { 147 | "name": "_ethAmount", 148 | "type": "uint256" 149 | } 150 | ], 151 | "name": "getSharesByPooledEth", 152 | "outputs": [ 153 | { 154 | "name": "", 155 | "type": "uint256" 156 | } 157 | ], 158 | "payable": false, 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "constant": true, 164 | "inputs": [], 165 | "name": "isStakingPaused", 166 | "outputs": [ 167 | { 168 | "name": "", 169 | "type": "bool" 170 | } 171 | ], 172 | "payable": false, 173 | "stateMutability": "view", 174 | "type": "function" 175 | }, 176 | { 177 | "constant": false, 178 | "inputs": [ 179 | { 180 | "name": "_sender", 181 | "type": "address" 182 | }, 183 | { 184 | "name": "_recipient", 185 | "type": "address" 186 | }, 187 | { 188 | "name": "_amount", 189 | "type": "uint256" 190 | } 191 | ], 192 | "name": "transferFrom", 193 | "outputs": [ 194 | { 195 | "name": "", 196 | "type": "bool" 197 | } 198 | ], 199 | "payable": false, 200 | "stateMutability": "nonpayable", 201 | "type": "function" 202 | }, 203 | { 204 | "constant": true, 205 | "inputs": [], 206 | "name": "getOperators", 207 | "outputs": [ 208 | { 209 | "name": "", 210 | "type": "address" 211 | } 212 | ], 213 | "payable": false, 214 | "stateMutability": "view", 215 | "type": "function" 216 | }, 217 | { 218 | "constant": true, 219 | "inputs": [ 220 | { 221 | "name": "_script", 222 | "type": "bytes" 223 | } 224 | ], 225 | "name": "getEVMScriptExecutor", 226 | "outputs": [ 227 | { 228 | "name": "", 229 | "type": "address" 230 | } 231 | ], 232 | "payable": false, 233 | "stateMutability": "view", 234 | "type": "function" 235 | }, 236 | { 237 | "constant": false, 238 | "inputs": [ 239 | { 240 | "name": "_maxStakeLimit", 241 | "type": "uint256" 242 | }, 243 | { 244 | "name": "_stakeLimitIncreasePerBlock", 245 | "type": "uint256" 246 | } 247 | ], 248 | "name": "setStakingLimit", 249 | "outputs": [], 250 | "payable": false, 251 | "stateMutability": "nonpayable", 252 | "type": "function" 253 | }, 254 | { 255 | "constant": true, 256 | "inputs": [], 257 | "name": "RESUME_ROLE", 258 | "outputs": [ 259 | { 260 | "name": "", 261 | "type": "bytes32" 262 | } 263 | ], 264 | "payable": false, 265 | "stateMutability": "view", 266 | "type": "function" 267 | }, 268 | { 269 | "constant": true, 270 | "inputs": [], 271 | "name": "decimals", 272 | "outputs": [ 273 | { 274 | "name": "", 275 | "type": "uint8" 276 | } 277 | ], 278 | "payable": false, 279 | "stateMutability": "pure", 280 | "type": "function" 281 | }, 282 | { 283 | "constant": true, 284 | "inputs": [], 285 | "name": "getRecoveryVault", 286 | "outputs": [ 287 | { 288 | "name": "", 289 | "type": "address" 290 | } 291 | ], 292 | "payable": false, 293 | "stateMutability": "view", 294 | "type": "function" 295 | }, 296 | { 297 | "constant": true, 298 | "inputs": [], 299 | "name": "DEPOSIT_ROLE", 300 | "outputs": [ 301 | { 302 | "name": "", 303 | "type": "bytes32" 304 | } 305 | ], 306 | "payable": false, 307 | "stateMutability": "view", 308 | "type": "function" 309 | }, 310 | { 311 | "constant": true, 312 | "inputs": [], 313 | "name": "DEPOSIT_SIZE", 314 | "outputs": [ 315 | { 316 | "name": "", 317 | "type": "uint256" 318 | } 319 | ], 320 | "payable": false, 321 | "stateMutability": "view", 322 | "type": "function" 323 | }, 324 | { 325 | "constant": true, 326 | "inputs": [], 327 | "name": "getTotalPooledEther", 328 | "outputs": [ 329 | { 330 | "name": "", 331 | "type": "uint256" 332 | } 333 | ], 334 | "payable": false, 335 | "stateMutability": "view", 336 | "type": "function" 337 | }, 338 | { 339 | "constant": true, 340 | "inputs": [], 341 | "name": "PAUSE_ROLE", 342 | "outputs": [ 343 | { 344 | "name": "", 345 | "type": "bytes32" 346 | } 347 | ], 348 | "payable": false, 349 | "stateMutability": "view", 350 | "type": "function" 351 | }, 352 | { 353 | "constant": false, 354 | "inputs": [ 355 | { 356 | "name": "_spender", 357 | "type": "address" 358 | }, 359 | { 360 | "name": "_addedValue", 361 | "type": "uint256" 362 | } 363 | ], 364 | "name": "increaseAllowance", 365 | "outputs": [ 366 | { 367 | "name": "", 368 | "type": "bool" 369 | } 370 | ], 371 | "payable": false, 372 | "stateMutability": "nonpayable", 373 | "type": "function" 374 | }, 375 | { 376 | "constant": true, 377 | "inputs": [], 378 | "name": "getTreasury", 379 | "outputs": [ 380 | { 381 | "name": "", 382 | "type": "address" 383 | } 384 | ], 385 | "payable": false, 386 | "stateMutability": "view", 387 | "type": "function" 388 | }, 389 | { 390 | "constant": true, 391 | "inputs": [], 392 | "name": "isStopped", 393 | "outputs": [ 394 | { 395 | "name": "", 396 | "type": "bool" 397 | } 398 | ], 399 | "payable": false, 400 | "stateMutability": "view", 401 | "type": "function" 402 | }, 403 | { 404 | "constant": true, 405 | "inputs": [], 406 | "name": "MANAGE_WITHDRAWAL_KEY", 407 | "outputs": [ 408 | { 409 | "name": "", 410 | "type": "bytes32" 411 | } 412 | ], 413 | "payable": false, 414 | "stateMutability": "view", 415 | "type": "function" 416 | }, 417 | { 418 | "constant": true, 419 | "inputs": [], 420 | "name": "getBufferedEther", 421 | "outputs": [ 422 | { 423 | "name": "", 424 | "type": "uint256" 425 | } 426 | ], 427 | "payable": false, 428 | "stateMutability": "view", 429 | "type": "function" 430 | }, 431 | { 432 | "constant": false, 433 | "inputs": [], 434 | "name": "receiveELRewards", 435 | "outputs": [], 436 | "payable": true, 437 | "stateMutability": "payable", 438 | "type": "function" 439 | }, 440 | { 441 | "constant": true, 442 | "inputs": [], 443 | "name": "getELRewardsWithdrawalLimit", 444 | "outputs": [ 445 | { 446 | "name": "", 447 | "type": "uint256" 448 | } 449 | ], 450 | "payable": false, 451 | "stateMutability": "view", 452 | "type": "function" 453 | }, 454 | { 455 | "constant": true, 456 | "inputs": [], 457 | "name": "SIGNATURE_LENGTH", 458 | "outputs": [ 459 | { 460 | "name": "", 461 | "type": "uint256" 462 | } 463 | ], 464 | "payable": false, 465 | "stateMutability": "view", 466 | "type": "function" 467 | }, 468 | { 469 | "constant": true, 470 | "inputs": [], 471 | "name": "getWithdrawalCredentials", 472 | "outputs": [ 473 | { 474 | "name": "", 475 | "type": "bytes32" 476 | } 477 | ], 478 | "payable": false, 479 | "stateMutability": "view", 480 | "type": "function" 481 | }, 482 | { 483 | "constant": true, 484 | "inputs": [], 485 | "name": "getCurrentStakeLimit", 486 | "outputs": [ 487 | { 488 | "name": "", 489 | "type": "uint256" 490 | } 491 | ], 492 | "payable": false, 493 | "stateMutability": "view", 494 | "type": "function" 495 | }, 496 | { 497 | "constant": false, 498 | "inputs": [ 499 | { 500 | "name": "_limitPoints", 501 | "type": "uint16" 502 | } 503 | ], 504 | "name": "setELRewardsWithdrawalLimit", 505 | "outputs": [], 506 | "payable": false, 507 | "stateMutability": "nonpayable", 508 | "type": "function" 509 | }, 510 | { 511 | "constant": false, 512 | "inputs": [ 513 | { 514 | "name": "_beaconValidators", 515 | "type": "uint256" 516 | }, 517 | { 518 | "name": "_beaconBalance", 519 | "type": "uint256" 520 | } 521 | ], 522 | "name": "handleOracleReport", 523 | "outputs": [], 524 | "payable": false, 525 | "stateMutability": "nonpayable", 526 | "type": "function" 527 | }, 528 | { 529 | "constant": true, 530 | "inputs": [], 531 | "name": "getStakeLimitFullInfo", 532 | "outputs": [ 533 | { 534 | "name": "isStakingPaused", 535 | "type": "bool" 536 | }, 537 | { 538 | "name": "isStakingLimitSet", 539 | "type": "bool" 540 | }, 541 | { 542 | "name": "currentStakeLimit", 543 | "type": "uint256" 544 | }, 545 | { 546 | "name": "maxStakeLimit", 547 | "type": "uint256" 548 | }, 549 | { 550 | "name": "maxStakeLimitGrowthBlocks", 551 | "type": "uint256" 552 | }, 553 | { 554 | "name": "prevStakeLimit", 555 | "type": "uint256" 556 | }, 557 | { 558 | "name": "prevStakeBlockNumber", 559 | "type": "uint256" 560 | } 561 | ], 562 | "payable": false, 563 | "stateMutability": "view", 564 | "type": "function" 565 | }, 566 | { 567 | "constant": true, 568 | "inputs": [], 569 | "name": "SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE", 570 | "outputs": [ 571 | { 572 | "name": "", 573 | "type": "bytes32" 574 | } 575 | ], 576 | "payable": false, 577 | "stateMutability": "view", 578 | "type": "function" 579 | }, 580 | { 581 | "constant": true, 582 | "inputs": [], 583 | "name": "getELRewardsVault", 584 | "outputs": [ 585 | { 586 | "name": "", 587 | "type": "address" 588 | } 589 | ], 590 | "payable": false, 591 | "stateMutability": "view", 592 | "type": "function" 593 | }, 594 | { 595 | "constant": true, 596 | "inputs": [ 597 | { 598 | "name": "_account", 599 | "type": "address" 600 | } 601 | ], 602 | "name": "balanceOf", 603 | "outputs": [ 604 | { 605 | "name": "", 606 | "type": "uint256" 607 | } 608 | ], 609 | "payable": false, 610 | "stateMutability": "view", 611 | "type": "function" 612 | }, 613 | { 614 | "constant": false, 615 | "inputs": [], 616 | "name": "resumeStaking", 617 | "outputs": [], 618 | "payable": false, 619 | "stateMutability": "nonpayable", 620 | "type": "function" 621 | }, 622 | { 623 | "constant": true, 624 | "inputs": [], 625 | "name": "getFeeDistribution", 626 | "outputs": [ 627 | { 628 | "name": "treasuryFeeBasisPoints", 629 | "type": "uint16" 630 | }, 631 | { 632 | "name": "insuranceFeeBasisPoints", 633 | "type": "uint16" 634 | }, 635 | { 636 | "name": "operatorsFeeBasisPoints", 637 | "type": "uint16" 638 | } 639 | ], 640 | "payable": false, 641 | "stateMutability": "view", 642 | "type": "function" 643 | }, 644 | { 645 | "constant": true, 646 | "inputs": [ 647 | { 648 | "name": "_sharesAmount", 649 | "type": "uint256" 650 | } 651 | ], 652 | "name": "getPooledEthByShares", 653 | "outputs": [ 654 | { 655 | "name": "", 656 | "type": "uint256" 657 | } 658 | ], 659 | "payable": false, 660 | "stateMutability": "view", 661 | "type": "function" 662 | }, 663 | { 664 | "constant": false, 665 | "inputs": [ 666 | { 667 | "name": "_executionLayerRewardsVault", 668 | "type": "address" 669 | } 670 | ], 671 | "name": "setELRewardsVault", 672 | "outputs": [], 673 | "payable": false, 674 | "stateMutability": "nonpayable", 675 | "type": "function" 676 | }, 677 | { 678 | "constant": true, 679 | "inputs": [ 680 | { 681 | "name": "token", 682 | "type": "address" 683 | } 684 | ], 685 | "name": "allowRecoverability", 686 | "outputs": [ 687 | { 688 | "name": "", 689 | "type": "bool" 690 | } 691 | ], 692 | "payable": false, 693 | "stateMutability": "view", 694 | "type": "function" 695 | }, 696 | { 697 | "constant": true, 698 | "inputs": [], 699 | "name": "MANAGE_PROTOCOL_CONTRACTS_ROLE", 700 | "outputs": [ 701 | { 702 | "name": "", 703 | "type": "bytes32" 704 | } 705 | ], 706 | "payable": false, 707 | "stateMutability": "view", 708 | "type": "function" 709 | }, 710 | { 711 | "constant": true, 712 | "inputs": [], 713 | "name": "appId", 714 | "outputs": [ 715 | { 716 | "name": "", 717 | "type": "bytes32" 718 | } 719 | ], 720 | "payable": false, 721 | "stateMutability": "view", 722 | "type": "function" 723 | }, 724 | { 725 | "constant": true, 726 | "inputs": [], 727 | "name": "getOracle", 728 | "outputs": [ 729 | { 730 | "name": "", 731 | "type": "address" 732 | } 733 | ], 734 | "payable": false, 735 | "stateMutability": "view", 736 | "type": "function" 737 | }, 738 | { 739 | "constant": true, 740 | "inputs": [], 741 | "name": "getInitializationBlock", 742 | "outputs": [ 743 | { 744 | "name": "", 745 | "type": "uint256" 746 | } 747 | ], 748 | "payable": false, 749 | "stateMutability": "view", 750 | "type": "function" 751 | }, 752 | { 753 | "constant": false, 754 | "inputs": [ 755 | { 756 | "name": "_treasuryFeeBasisPoints", 757 | "type": "uint16" 758 | }, 759 | { 760 | "name": "_insuranceFeeBasisPoints", 761 | "type": "uint16" 762 | }, 763 | { 764 | "name": "_operatorsFeeBasisPoints", 765 | "type": "uint16" 766 | } 767 | ], 768 | "name": "setFeeDistribution", 769 | "outputs": [], 770 | "payable": false, 771 | "stateMutability": "nonpayable", 772 | "type": "function" 773 | }, 774 | { 775 | "constant": false, 776 | "inputs": [ 777 | { 778 | "name": "_feeBasisPoints", 779 | "type": "uint16" 780 | } 781 | ], 782 | "name": "setFee", 783 | "outputs": [], 784 | "payable": false, 785 | "stateMutability": "nonpayable", 786 | "type": "function" 787 | }, 788 | { 789 | "constant": false, 790 | "inputs": [ 791 | { 792 | "name": "_recipient", 793 | "type": "address" 794 | }, 795 | { 796 | "name": "_sharesAmount", 797 | "type": "uint256" 798 | } 799 | ], 800 | "name": "transferShares", 801 | "outputs": [ 802 | { 803 | "name": "", 804 | "type": "uint256" 805 | } 806 | ], 807 | "payable": false, 808 | "stateMutability": "nonpayable", 809 | "type": "function" 810 | }, 811 | { 812 | "constant": false, 813 | "inputs": [ 814 | { 815 | "name": "_maxDeposits", 816 | "type": "uint256" 817 | } 818 | ], 819 | "name": "depositBufferedEther", 820 | "outputs": [], 821 | "payable": false, 822 | "stateMutability": "nonpayable", 823 | "type": "function" 824 | }, 825 | { 826 | "constant": true, 827 | "inputs": [], 828 | "name": "symbol", 829 | "outputs": [ 830 | { 831 | "name": "", 832 | "type": "string" 833 | } 834 | ], 835 | "payable": false, 836 | "stateMutability": "pure", 837 | "type": "function" 838 | }, 839 | { 840 | "constant": true, 841 | "inputs": [], 842 | "name": "MANAGE_FEE", 843 | "outputs": [ 844 | { 845 | "name": "", 846 | "type": "bytes32" 847 | } 848 | ], 849 | "payable": false, 850 | "stateMutability": "view", 851 | "type": "function" 852 | }, 853 | { 854 | "constant": false, 855 | "inputs": [ 856 | { 857 | "name": "_token", 858 | "type": "address" 859 | } 860 | ], 861 | "name": "transferToVault", 862 | "outputs": [], 863 | "payable": false, 864 | "stateMutability": "nonpayable", 865 | "type": "function" 866 | }, 867 | { 868 | "constant": true, 869 | "inputs": [ 870 | { 871 | "name": "_sender", 872 | "type": "address" 873 | }, 874 | { 875 | "name": "_role", 876 | "type": "bytes32" 877 | }, 878 | { 879 | "name": "_params", 880 | "type": "uint256[]" 881 | } 882 | ], 883 | "name": "canPerform", 884 | "outputs": [ 885 | { 886 | "name": "", 887 | "type": "bool" 888 | } 889 | ], 890 | "payable": false, 891 | "stateMutability": "view", 892 | "type": "function" 893 | }, 894 | { 895 | "constant": false, 896 | "inputs": [ 897 | { 898 | "name": "_referral", 899 | "type": "address" 900 | } 901 | ], 902 | "name": "submit", 903 | "outputs": [ 904 | { 905 | "name": "", 906 | "type": "uint256" 907 | } 908 | ], 909 | "payable": true, 910 | "stateMutability": "payable", 911 | "type": "function" 912 | }, 913 | { 914 | "constant": true, 915 | "inputs": [], 916 | "name": "WITHDRAWAL_CREDENTIALS_LENGTH", 917 | "outputs": [ 918 | { 919 | "name": "", 920 | "type": "uint256" 921 | } 922 | ], 923 | "payable": false, 924 | "stateMutability": "view", 925 | "type": "function" 926 | }, 927 | { 928 | "constant": false, 929 | "inputs": [ 930 | { 931 | "name": "_spender", 932 | "type": "address" 933 | }, 934 | { 935 | "name": "_subtractedValue", 936 | "type": "uint256" 937 | } 938 | ], 939 | "name": "decreaseAllowance", 940 | "outputs": [ 941 | { 942 | "name": "", 943 | "type": "bool" 944 | } 945 | ], 946 | "payable": false, 947 | "stateMutability": "nonpayable", 948 | "type": "function" 949 | }, 950 | { 951 | "constant": true, 952 | "inputs": [], 953 | "name": "getEVMScriptRegistry", 954 | "outputs": [ 955 | { 956 | "name": "", 957 | "type": "address" 958 | } 959 | ], 960 | "payable": false, 961 | "stateMutability": "view", 962 | "type": "function" 963 | }, 964 | { 965 | "constant": true, 966 | "inputs": [], 967 | "name": "PUBKEY_LENGTH", 968 | "outputs": [ 969 | { 970 | "name": "", 971 | "type": "uint256" 972 | } 973 | ], 974 | "payable": false, 975 | "stateMutability": "view", 976 | "type": "function" 977 | }, 978 | { 979 | "constant": true, 980 | "inputs": [], 981 | "name": "SET_EL_REWARDS_VAULT_ROLE", 982 | "outputs": [ 983 | { 984 | "name": "", 985 | "type": "bytes32" 986 | } 987 | ], 988 | "payable": false, 989 | "stateMutability": "view", 990 | "type": "function" 991 | }, 992 | { 993 | "constant": false, 994 | "inputs": [ 995 | { 996 | "name": "_recipient", 997 | "type": "address" 998 | }, 999 | { 1000 | "name": "_amount", 1001 | "type": "uint256" 1002 | } 1003 | ], 1004 | "name": "transfer", 1005 | "outputs": [ 1006 | { 1007 | "name": "", 1008 | "type": "bool" 1009 | } 1010 | ], 1011 | "payable": false, 1012 | "stateMutability": "nonpayable", 1013 | "type": "function" 1014 | }, 1015 | { 1016 | "constant": true, 1017 | "inputs": [], 1018 | "name": "getDepositContract", 1019 | "outputs": [ 1020 | { 1021 | "name": "", 1022 | "type": "address" 1023 | } 1024 | ], 1025 | "payable": false, 1026 | "stateMutability": "view", 1027 | "type": "function" 1028 | }, 1029 | { 1030 | "constant": true, 1031 | "inputs": [], 1032 | "name": "getBeaconStat", 1033 | "outputs": [ 1034 | { 1035 | "name": "depositedValidators", 1036 | "type": "uint256" 1037 | }, 1038 | { 1039 | "name": "beaconValidators", 1040 | "type": "uint256" 1041 | }, 1042 | { 1043 | "name": "beaconBalance", 1044 | "type": "uint256" 1045 | } 1046 | ], 1047 | "payable": false, 1048 | "stateMutability": "view", 1049 | "type": "function" 1050 | }, 1051 | { 1052 | "constant": false, 1053 | "inputs": [], 1054 | "name": "removeStakingLimit", 1055 | "outputs": [], 1056 | "payable": false, 1057 | "stateMutability": "nonpayable", 1058 | "type": "function" 1059 | }, 1060 | { 1061 | "constant": true, 1062 | "inputs": [], 1063 | "name": "BURN_ROLE", 1064 | "outputs": [ 1065 | { 1066 | "name": "", 1067 | "type": "bytes32" 1068 | } 1069 | ], 1070 | "payable": false, 1071 | "stateMutability": "view", 1072 | "type": "function" 1073 | }, 1074 | { 1075 | "constant": true, 1076 | "inputs": [], 1077 | "name": "getFee", 1078 | "outputs": [ 1079 | { 1080 | "name": "feeBasisPoints", 1081 | "type": "uint16" 1082 | } 1083 | ], 1084 | "payable": false, 1085 | "stateMutability": "view", 1086 | "type": "function" 1087 | }, 1088 | { 1089 | "constant": true, 1090 | "inputs": [], 1091 | "name": "kernel", 1092 | "outputs": [ 1093 | { 1094 | "name": "", 1095 | "type": "address" 1096 | } 1097 | ], 1098 | "payable": false, 1099 | "stateMutability": "view", 1100 | "type": "function" 1101 | }, 1102 | { 1103 | "constant": true, 1104 | "inputs": [], 1105 | "name": "getTotalShares", 1106 | "outputs": [ 1107 | { 1108 | "name": "", 1109 | "type": "uint256" 1110 | } 1111 | ], 1112 | "payable": false, 1113 | "stateMutability": "view", 1114 | "type": "function" 1115 | }, 1116 | { 1117 | "constant": true, 1118 | "inputs": [ 1119 | { 1120 | "name": "_owner", 1121 | "type": "address" 1122 | }, 1123 | { 1124 | "name": "_spender", 1125 | "type": "address" 1126 | } 1127 | ], 1128 | "name": "allowance", 1129 | "outputs": [ 1130 | { 1131 | "name": "", 1132 | "type": "uint256" 1133 | } 1134 | ], 1135 | "payable": false, 1136 | "stateMutability": "view", 1137 | "type": "function" 1138 | }, 1139 | { 1140 | "constant": true, 1141 | "inputs": [], 1142 | "name": "isPetrified", 1143 | "outputs": [ 1144 | { 1145 | "name": "", 1146 | "type": "bool" 1147 | } 1148 | ], 1149 | "payable": false, 1150 | "stateMutability": "view", 1151 | "type": "function" 1152 | }, 1153 | { 1154 | "constant": false, 1155 | "inputs": [ 1156 | { 1157 | "name": "_oracle", 1158 | "type": "address" 1159 | }, 1160 | { 1161 | "name": "_treasury", 1162 | "type": "address" 1163 | }, 1164 | { 1165 | "name": "_insuranceFund", 1166 | "type": "address" 1167 | } 1168 | ], 1169 | "name": "setProtocolContracts", 1170 | "outputs": [], 1171 | "payable": false, 1172 | "stateMutability": "nonpayable", 1173 | "type": "function" 1174 | }, 1175 | { 1176 | "constant": false, 1177 | "inputs": [ 1178 | { 1179 | "name": "_withdrawalCredentials", 1180 | "type": "bytes32" 1181 | } 1182 | ], 1183 | "name": "setWithdrawalCredentials", 1184 | "outputs": [], 1185 | "payable": false, 1186 | "stateMutability": "nonpayable", 1187 | "type": "function" 1188 | }, 1189 | { 1190 | "constant": true, 1191 | "inputs": [], 1192 | "name": "STAKING_PAUSE_ROLE", 1193 | "outputs": [ 1194 | { 1195 | "name": "", 1196 | "type": "bytes32" 1197 | } 1198 | ], 1199 | "payable": false, 1200 | "stateMutability": "view", 1201 | "type": "function" 1202 | }, 1203 | { 1204 | "constant": false, 1205 | "inputs": [], 1206 | "name": "depositBufferedEther", 1207 | "outputs": [], 1208 | "payable": false, 1209 | "stateMutability": "nonpayable", 1210 | "type": "function" 1211 | }, 1212 | { 1213 | "constant": false, 1214 | "inputs": [ 1215 | { 1216 | "name": "_account", 1217 | "type": "address" 1218 | }, 1219 | { 1220 | "name": "_sharesAmount", 1221 | "type": "uint256" 1222 | } 1223 | ], 1224 | "name": "burnShares", 1225 | "outputs": [ 1226 | { 1227 | "name": "newTotalShares", 1228 | "type": "uint256" 1229 | } 1230 | ], 1231 | "payable": false, 1232 | "stateMutability": "nonpayable", 1233 | "type": "function" 1234 | }, 1235 | { 1236 | "constant": true, 1237 | "inputs": [ 1238 | { 1239 | "name": "_account", 1240 | "type": "address" 1241 | } 1242 | ], 1243 | "name": "sharesOf", 1244 | "outputs": [ 1245 | { 1246 | "name": "", 1247 | "type": "uint256" 1248 | } 1249 | ], 1250 | "payable": false, 1251 | "stateMutability": "view", 1252 | "type": "function" 1253 | }, 1254 | { 1255 | "constant": false, 1256 | "inputs": [], 1257 | "name": "pauseStaking", 1258 | "outputs": [], 1259 | "payable": false, 1260 | "stateMutability": "nonpayable", 1261 | "type": "function" 1262 | }, 1263 | { 1264 | "constant": true, 1265 | "inputs": [], 1266 | "name": "getTotalELRewardsCollected", 1267 | "outputs": [ 1268 | { 1269 | "name": "", 1270 | "type": "uint256" 1271 | } 1272 | ], 1273 | "payable": false, 1274 | "stateMutability": "view", 1275 | "type": "function" 1276 | }, 1277 | { 1278 | "payable": true, 1279 | "stateMutability": "payable", 1280 | "type": "fallback" 1281 | }, 1282 | { 1283 | "anonymous": false, 1284 | "inputs": [ 1285 | { 1286 | "indexed": true, 1287 | "name": "executor", 1288 | "type": "address" 1289 | }, 1290 | { 1291 | "indexed": false, 1292 | "name": "script", 1293 | "type": "bytes" 1294 | }, 1295 | { 1296 | "indexed": false, 1297 | "name": "input", 1298 | "type": "bytes" 1299 | }, 1300 | { 1301 | "indexed": false, 1302 | "name": "returnData", 1303 | "type": "bytes" 1304 | } 1305 | ], 1306 | "name": "ScriptResult", 1307 | "type": "event" 1308 | }, 1309 | { 1310 | "anonymous": false, 1311 | "inputs": [ 1312 | { 1313 | "indexed": true, 1314 | "name": "vault", 1315 | "type": "address" 1316 | }, 1317 | { 1318 | "indexed": true, 1319 | "name": "token", 1320 | "type": "address" 1321 | }, 1322 | { 1323 | "indexed": false, 1324 | "name": "amount", 1325 | "type": "uint256" 1326 | } 1327 | ], 1328 | "name": "RecoverToVault", 1329 | "type": "event" 1330 | }, 1331 | { 1332 | "anonymous": false, 1333 | "inputs": [ 1334 | { 1335 | "indexed": true, 1336 | "name": "from", 1337 | "type": "address" 1338 | }, 1339 | { 1340 | "indexed": true, 1341 | "name": "to", 1342 | "type": "address" 1343 | }, 1344 | { 1345 | "indexed": false, 1346 | "name": "sharesValue", 1347 | "type": "uint256" 1348 | } 1349 | ], 1350 | "name": "TransferShares", 1351 | "type": "event" 1352 | }, 1353 | { 1354 | "anonymous": false, 1355 | "inputs": [ 1356 | { 1357 | "indexed": true, 1358 | "name": "account", 1359 | "type": "address" 1360 | }, 1361 | { 1362 | "indexed": false, 1363 | "name": "preRebaseTokenAmount", 1364 | "type": "uint256" 1365 | }, 1366 | { 1367 | "indexed": false, 1368 | "name": "postRebaseTokenAmount", 1369 | "type": "uint256" 1370 | }, 1371 | { 1372 | "indexed": false, 1373 | "name": "sharesAmount", 1374 | "type": "uint256" 1375 | } 1376 | ], 1377 | "name": "SharesBurnt", 1378 | "type": "event" 1379 | }, 1380 | { 1381 | "anonymous": false, 1382 | "inputs": [ 1383 | { 1384 | "indexed": true, 1385 | "name": "from", 1386 | "type": "address" 1387 | }, 1388 | { 1389 | "indexed": true, 1390 | "name": "to", 1391 | "type": "address" 1392 | }, 1393 | { 1394 | "indexed": false, 1395 | "name": "value", 1396 | "type": "uint256" 1397 | } 1398 | ], 1399 | "name": "Transfer", 1400 | "type": "event" 1401 | }, 1402 | { 1403 | "anonymous": false, 1404 | "inputs": [ 1405 | { 1406 | "indexed": true, 1407 | "name": "owner", 1408 | "type": "address" 1409 | }, 1410 | { 1411 | "indexed": true, 1412 | "name": "spender", 1413 | "type": "address" 1414 | }, 1415 | { 1416 | "indexed": false, 1417 | "name": "value", 1418 | "type": "uint256" 1419 | } 1420 | ], 1421 | "name": "Approval", 1422 | "type": "event" 1423 | }, 1424 | { 1425 | "anonymous": false, 1426 | "inputs": [ 1427 | { 1428 | "indexed": false, 1429 | "name": "maxStakeLimit", 1430 | "type": "uint256" 1431 | }, 1432 | { 1433 | "indexed": false, 1434 | "name": "stakeLimitIncreasePerBlock", 1435 | "type": "uint256" 1436 | } 1437 | ], 1438 | "name": "StakingLimitSet", 1439 | "type": "event" 1440 | }, 1441 | { 1442 | "anonymous": false, 1443 | "inputs": [ 1444 | { 1445 | "indexed": false, 1446 | "name": "oracle", 1447 | "type": "address" 1448 | }, 1449 | { 1450 | "indexed": false, 1451 | "name": "treasury", 1452 | "type": "address" 1453 | }, 1454 | { 1455 | "indexed": false, 1456 | "name": "insuranceFund", 1457 | "type": "address" 1458 | } 1459 | ], 1460 | "name": "ProtocolContactsSet", 1461 | "type": "event" 1462 | }, 1463 | { 1464 | "anonymous": false, 1465 | "inputs": [ 1466 | { 1467 | "indexed": false, 1468 | "name": "feeBasisPoints", 1469 | "type": "uint16" 1470 | } 1471 | ], 1472 | "name": "FeeSet", 1473 | "type": "event" 1474 | }, 1475 | { 1476 | "anonymous": false, 1477 | "inputs": [ 1478 | { 1479 | "indexed": false, 1480 | "name": "treasuryFeeBasisPoints", 1481 | "type": "uint16" 1482 | }, 1483 | { 1484 | "indexed": false, 1485 | "name": "insuranceFeeBasisPoints", 1486 | "type": "uint16" 1487 | }, 1488 | { 1489 | "indexed": false, 1490 | "name": "operatorsFeeBasisPoints", 1491 | "type": "uint16" 1492 | } 1493 | ], 1494 | "name": "FeeDistributionSet", 1495 | "type": "event" 1496 | }, 1497 | { 1498 | "anonymous": false, 1499 | "inputs": [ 1500 | { 1501 | "indexed": false, 1502 | "name": "amount", 1503 | "type": "uint256" 1504 | } 1505 | ], 1506 | "name": "ELRewardsReceived", 1507 | "type": "event" 1508 | }, 1509 | { 1510 | "anonymous": false, 1511 | "inputs": [ 1512 | { 1513 | "indexed": false, 1514 | "name": "limitPoints", 1515 | "type": "uint256" 1516 | } 1517 | ], 1518 | "name": "ELRewardsWithdrawalLimitSet", 1519 | "type": "event" 1520 | }, 1521 | { 1522 | "anonymous": false, 1523 | "inputs": [ 1524 | { 1525 | "indexed": false, 1526 | "name": "withdrawalCredentials", 1527 | "type": "bytes32" 1528 | } 1529 | ], 1530 | "name": "WithdrawalCredentialsSet", 1531 | "type": "event" 1532 | }, 1533 | { 1534 | "anonymous": false, 1535 | "inputs": [ 1536 | { 1537 | "indexed": false, 1538 | "name": "executionLayerRewardsVault", 1539 | "type": "address" 1540 | } 1541 | ], 1542 | "name": "ELRewardsVaultSet", 1543 | "type": "event" 1544 | }, 1545 | { 1546 | "anonymous": false, 1547 | "inputs": [ 1548 | { 1549 | "indexed": true, 1550 | "name": "sender", 1551 | "type": "address" 1552 | }, 1553 | { 1554 | "indexed": false, 1555 | "name": "amount", 1556 | "type": "uint256" 1557 | }, 1558 | { 1559 | "indexed": false, 1560 | "name": "referral", 1561 | "type": "address" 1562 | } 1563 | ], 1564 | "name": "Submitted", 1565 | "type": "event" 1566 | }, 1567 | { 1568 | "anonymous": false, 1569 | "inputs": [ 1570 | { 1571 | "indexed": false, 1572 | "name": "amount", 1573 | "type": "uint256" 1574 | } 1575 | ], 1576 | "name": "Unbuffered", 1577 | "type": "event" 1578 | }, 1579 | { 1580 | "anonymous": false, 1581 | "inputs": [ 1582 | { 1583 | "indexed": true, 1584 | "name": "sender", 1585 | "type": "address" 1586 | }, 1587 | { 1588 | "indexed": false, 1589 | "name": "tokenAmount", 1590 | "type": "uint256" 1591 | }, 1592 | { 1593 | "indexed": false, 1594 | "name": "sentFromBuffer", 1595 | "type": "uint256" 1596 | }, 1597 | { 1598 | "indexed": true, 1599 | "name": "pubkeyHash", 1600 | "type": "bytes32" 1601 | }, 1602 | { 1603 | "indexed": false, 1604 | "name": "etherAmount", 1605 | "type": "uint256" 1606 | } 1607 | ], 1608 | "name": "Withdrawal", 1609 | "type": "event" 1610 | } 1611 | ] 1612 | -------------------------------------------------------------------------------- /lib/abi/LidoOracle.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "getCurrentOraclesReportStatus", 6 | "outputs": [{ "name": "", "type": "uint256" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [{ "name": "_value", "type": "uint256" }], 14 | "name": "setAllowedBeaconBalanceAnnualRelativeIncrease", 15 | "outputs": [], 16 | "payable": false, 17 | "stateMutability": "nonpayable", 18 | "type": "function" 19 | }, 20 | { 21 | "constant": true, 22 | "inputs": [], 23 | "name": "hasInitialized", 24 | "outputs": [{ "name": "", "type": "bool" }], 25 | "payable": false, 26 | "stateMutability": "view", 27 | "type": "function" 28 | }, 29 | { 30 | "constant": true, 31 | "inputs": [], 32 | "name": "getVersion", 33 | "outputs": [{ "name": "", "type": "uint256" }], 34 | "payable": false, 35 | "stateMutability": "view", 36 | "type": "function" 37 | }, 38 | { 39 | "constant": true, 40 | "inputs": [{ "name": "_script", "type": "bytes" }], 41 | "name": "getEVMScriptExecutor", 42 | "outputs": [{ "name": "", "type": "address" }], 43 | "payable": false, 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": true, 49 | "inputs": [], 50 | "name": "MANAGE_QUORUM", 51 | "outputs": [{ "name": "", "type": "bytes32" }], 52 | "payable": false, 53 | "stateMutability": "view", 54 | "type": "function" 55 | }, 56 | { 57 | "constant": false, 58 | "inputs": [ 59 | { "name": "_epochId", "type": "uint256" }, 60 | { "name": "_beaconBalance", "type": "uint64" }, 61 | { "name": "_beaconValidators", "type": "uint32" } 62 | ], 63 | "name": "reportBeacon", 64 | "outputs": [], 65 | "payable": false, 66 | "stateMutability": "nonpayable", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": true, 71 | "inputs": [], 72 | "name": "getRecoveryVault", 73 | "outputs": [{ "name": "", "type": "address" }], 74 | "payable": false, 75 | "stateMutability": "view", 76 | "type": "function" 77 | }, 78 | { 79 | "constant": true, 80 | "inputs": [], 81 | "name": "getAllowedBeaconBalanceAnnualRelativeIncrease", 82 | "outputs": [{ "name": "", "type": "uint256" }], 83 | "payable": false, 84 | "stateMutability": "view", 85 | "type": "function" 86 | }, 87 | { 88 | "constant": true, 89 | "inputs": [], 90 | "name": "getAllowedBeaconBalanceRelativeDecrease", 91 | "outputs": [{ "name": "", "type": "uint256" }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": true, 98 | "inputs": [], 99 | "name": "getExpectedEpochId", 100 | "outputs": [{ "name": "", "type": "uint256" }], 101 | "payable": false, 102 | "stateMutability": "view", 103 | "type": "function" 104 | }, 105 | { 106 | "constant": true, 107 | "inputs": [], 108 | "name": "getLastCompletedReportDelta", 109 | "outputs": [ 110 | { "name": "postTotalPooledEther", "type": "uint256" }, 111 | { "name": "preTotalPooledEther", "type": "uint256" }, 112 | { "name": "timeElapsed", "type": "uint256" } 113 | ], 114 | "payable": false, 115 | "stateMutability": "view", 116 | "type": "function" 117 | }, 118 | { 119 | "constant": false, 120 | "inputs": [ 121 | { "name": "_lido", "type": "address" }, 122 | { "name": "_epochsPerFrame", "type": "uint64" }, 123 | { "name": "_slotsPerEpoch", "type": "uint64" }, 124 | { "name": "_secondsPerSlot", "type": "uint64" }, 125 | { "name": "_genesisTime", "type": "uint64" }, 126 | { "name": "_allowedBeaconBalanceAnnualRelativeIncrease", "type": "uint256" }, 127 | { "name": "_allowedBeaconBalanceRelativeDecrease", "type": "uint256" } 128 | ], 129 | "name": "initialize", 130 | "outputs": [], 131 | "payable": false, 132 | "stateMutability": "nonpayable", 133 | "type": "function" 134 | }, 135 | { 136 | "constant": true, 137 | "inputs": [], 138 | "name": "getLido", 139 | "outputs": [{ "name": "", "type": "address" }], 140 | "payable": false, 141 | "stateMutability": "view", 142 | "type": "function" 143 | }, 144 | { 145 | "constant": true, 146 | "inputs": [], 147 | "name": "SET_BEACON_REPORT_RECEIVER", 148 | "outputs": [{ "name": "", "type": "bytes32" }], 149 | "payable": false, 150 | "stateMutability": "view", 151 | "type": "function" 152 | }, 153 | { 154 | "constant": false, 155 | "inputs": [], 156 | "name": "finalizeUpgrade_v3", 157 | "outputs": [], 158 | "payable": false, 159 | "stateMutability": "nonpayable", 160 | "type": "function" 161 | }, 162 | { 163 | "constant": true, 164 | "inputs": [], 165 | "name": "MANAGE_MEMBERS", 166 | "outputs": [{ "name": "", "type": "bytes32" }], 167 | "payable": false, 168 | "stateMutability": "view", 169 | "type": "function" 170 | }, 171 | { 172 | "constant": true, 173 | "inputs": [], 174 | "name": "getCurrentFrame", 175 | "outputs": [ 176 | { "name": "frameEpochId", "type": "uint256" }, 177 | { "name": "frameStartTime", "type": "uint256" }, 178 | { "name": "frameEndTime", "type": "uint256" } 179 | ], 180 | "payable": false, 181 | "stateMutability": "view", 182 | "type": "function" 183 | }, 184 | { 185 | "constant": true, 186 | "inputs": [{ "name": "token", "type": "address" }], 187 | "name": "allowRecoverability", 188 | "outputs": [{ "name": "", "type": "bool" }], 189 | "payable": false, 190 | "stateMutability": "view", 191 | "type": "function" 192 | }, 193 | { 194 | "constant": true, 195 | "inputs": [{ "name": "_index", "type": "uint256" }], 196 | "name": "getCurrentReportVariant", 197 | "outputs": [ 198 | { "name": "beaconBalance", "type": "uint64" }, 199 | { "name": "beaconValidators", "type": "uint32" }, 200 | { "name": "count", "type": "uint16" } 201 | ], 202 | "payable": false, 203 | "stateMutability": "view", 204 | "type": "function" 205 | }, 206 | { 207 | "constant": true, 208 | "inputs": [], 209 | "name": "appId", 210 | "outputs": [{ "name": "", "type": "bytes32" }], 211 | "payable": false, 212 | "stateMutability": "view", 213 | "type": "function" 214 | }, 215 | { 216 | "constant": true, 217 | "inputs": [], 218 | "name": "getLastCompletedEpochId", 219 | "outputs": [{ "name": "", "type": "uint256" }], 220 | "payable": false, 221 | "stateMutability": "view", 222 | "type": "function" 223 | }, 224 | { 225 | "constant": true, 226 | "inputs": [], 227 | "name": "getInitializationBlock", 228 | "outputs": [{ "name": "", "type": "uint256" }], 229 | "payable": false, 230 | "stateMutability": "view", 231 | "type": "function" 232 | }, 233 | { 234 | "constant": false, 235 | "inputs": [{ "name": "_addr", "type": "address" }], 236 | "name": "setBeaconReportReceiver", 237 | "outputs": [], 238 | "payable": false, 239 | "stateMutability": "nonpayable", 240 | "type": "function" 241 | }, 242 | { 243 | "constant": false, 244 | "inputs": [{ "name": "_token", "type": "address" }], 245 | "name": "transferToVault", 246 | "outputs": [], 247 | "payable": false, 248 | "stateMutability": "nonpayable", 249 | "type": "function" 250 | }, 251 | { 252 | "constant": true, 253 | "inputs": [], 254 | "name": "SET_BEACON_SPEC", 255 | "outputs": [{ "name": "", "type": "bytes32" }], 256 | "payable": false, 257 | "stateMutability": "view", 258 | "type": "function" 259 | }, 260 | { 261 | "constant": true, 262 | "inputs": [ 263 | { "name": "_sender", "type": "address" }, 264 | { "name": "_role", "type": "bytes32" }, 265 | { "name": "_params", "type": "uint256[]" } 266 | ], 267 | "name": "canPerform", 268 | "outputs": [{ "name": "", "type": "bool" }], 269 | "payable": false, 270 | "stateMutability": "view", 271 | "type": "function" 272 | }, 273 | { 274 | "constant": true, 275 | "inputs": [], 276 | "name": "getCurrentEpochId", 277 | "outputs": [{ "name": "", "type": "uint256" }], 278 | "payable": false, 279 | "stateMutability": "view", 280 | "type": "function" 281 | }, 282 | { 283 | "constant": true, 284 | "inputs": [], 285 | "name": "getEVMScriptRegistry", 286 | "outputs": [{ "name": "", "type": "address" }], 287 | "payable": false, 288 | "stateMutability": "view", 289 | "type": "function" 290 | }, 291 | { 292 | "constant": false, 293 | "inputs": [{ "name": "_member", "type": "address" }], 294 | "name": "addOracleMember", 295 | "outputs": [], 296 | "payable": false, 297 | "stateMutability": "nonpayable", 298 | "type": "function" 299 | }, 300 | { 301 | "constant": true, 302 | "inputs": [], 303 | "name": "getBeaconReportReceiver", 304 | "outputs": [{ "name": "", "type": "address" }], 305 | "payable": false, 306 | "stateMutability": "view", 307 | "type": "function" 308 | }, 309 | { 310 | "constant": true, 311 | "inputs": [], 312 | "name": "SET_REPORT_BOUNDARIES", 313 | "outputs": [{ "name": "", "type": "bytes32" }], 314 | "payable": false, 315 | "stateMutability": "view", 316 | "type": "function" 317 | }, 318 | { 319 | "constant": false, 320 | "inputs": [{ "name": "_quorum", "type": "uint256" }], 321 | "name": "setQuorum", 322 | "outputs": [], 323 | "payable": false, 324 | "stateMutability": "nonpayable", 325 | "type": "function" 326 | }, 327 | { 328 | "constant": true, 329 | "inputs": [], 330 | "name": "getQuorum", 331 | "outputs": [{ "name": "", "type": "uint256" }], 332 | "payable": false, 333 | "stateMutability": "view", 334 | "type": "function" 335 | }, 336 | { 337 | "constant": true, 338 | "inputs": [], 339 | "name": "kernel", 340 | "outputs": [{ "name": "", "type": "address" }], 341 | "payable": false, 342 | "stateMutability": "view", 343 | "type": "function" 344 | }, 345 | { 346 | "constant": true, 347 | "inputs": [], 348 | "name": "getOracleMembers", 349 | "outputs": [{ "name": "", "type": "address[]" }], 350 | "payable": false, 351 | "stateMutability": "view", 352 | "type": "function" 353 | }, 354 | { 355 | "constant": true, 356 | "inputs": [], 357 | "name": "isPetrified", 358 | "outputs": [{ "name": "", "type": "bool" }], 359 | "payable": false, 360 | "stateMutability": "view", 361 | "type": "function" 362 | }, 363 | { 364 | "constant": false, 365 | "inputs": [{ "name": "_value", "type": "uint256" }], 366 | "name": "setAllowedBeaconBalanceRelativeDecrease", 367 | "outputs": [], 368 | "payable": false, 369 | "stateMutability": "nonpayable", 370 | "type": "function" 371 | }, 372 | { 373 | "constant": true, 374 | "inputs": [], 375 | "name": "getBeaconSpec", 376 | "outputs": [ 377 | { "name": "epochsPerFrame", "type": "uint64" }, 378 | { "name": "slotsPerEpoch", "type": "uint64" }, 379 | { "name": "secondsPerSlot", "type": "uint64" }, 380 | { "name": "genesisTime", "type": "uint64" } 381 | ], 382 | "payable": false, 383 | "stateMutability": "view", 384 | "type": "function" 385 | }, 386 | { 387 | "constant": false, 388 | "inputs": [ 389 | { "name": "_epochsPerFrame", "type": "uint64" }, 390 | { "name": "_slotsPerEpoch", "type": "uint64" }, 391 | { "name": "_secondsPerSlot", "type": "uint64" }, 392 | { "name": "_genesisTime", "type": "uint64" } 393 | ], 394 | "name": "setBeaconSpec", 395 | "outputs": [], 396 | "payable": false, 397 | "stateMutability": "nonpayable", 398 | "type": "function" 399 | }, 400 | { 401 | "constant": true, 402 | "inputs": [], 403 | "name": "MAX_MEMBERS", 404 | "outputs": [{ "name": "", "type": "uint256" }], 405 | "payable": false, 406 | "stateMutability": "view", 407 | "type": "function" 408 | }, 409 | { 410 | "constant": true, 411 | "inputs": [], 412 | "name": "getCurrentReportVariantsSize", 413 | "outputs": [{ "name": "", "type": "uint256" }], 414 | "payable": false, 415 | "stateMutability": "view", 416 | "type": "function" 417 | }, 418 | { 419 | "constant": false, 420 | "inputs": [{ "name": "_member", "type": "address" }], 421 | "name": "removeOracleMember", 422 | "outputs": [], 423 | "payable": false, 424 | "stateMutability": "nonpayable", 425 | "type": "function" 426 | }, 427 | { 428 | "anonymous": false, 429 | "inputs": [ 430 | { "indexed": true, "name": "executor", "type": "address" }, 431 | { "indexed": false, "name": "script", "type": "bytes" }, 432 | { "indexed": false, "name": "input", "type": "bytes" }, 433 | { "indexed": false, "name": "returnData", "type": "bytes" } 434 | ], 435 | "name": "ScriptResult", 436 | "type": "event" 437 | }, 438 | { 439 | "anonymous": false, 440 | "inputs": [ 441 | { "indexed": true, "name": "vault", "type": "address" }, 442 | { "indexed": true, "name": "token", "type": "address" }, 443 | { "indexed": false, "name": "amount", "type": "uint256" } 444 | ], 445 | "name": "RecoverToVault", 446 | "type": "event" 447 | }, 448 | { 449 | "anonymous": false, 450 | "inputs": [{ "indexed": false, "name": "value", "type": "uint256" }], 451 | "name": "AllowedBeaconBalanceAnnualRelativeIncreaseSet", 452 | "type": "event" 453 | }, 454 | { 455 | "anonymous": false, 456 | "inputs": [{ "indexed": false, "name": "value", "type": "uint256" }], 457 | "name": "AllowedBeaconBalanceRelativeDecreaseSet", 458 | "type": "event" 459 | }, 460 | { 461 | "anonymous": false, 462 | "inputs": [{ "indexed": false, "name": "callback", "type": "address" }], 463 | "name": "BeaconReportReceiverSet", 464 | "type": "event" 465 | }, 466 | { 467 | "anonymous": false, 468 | "inputs": [{ "indexed": false, "name": "member", "type": "address" }], 469 | "name": "MemberAdded", 470 | "type": "event" 471 | }, 472 | { 473 | "anonymous": false, 474 | "inputs": [{ "indexed": false, "name": "member", "type": "address" }], 475 | "name": "MemberRemoved", 476 | "type": "event" 477 | }, 478 | { 479 | "anonymous": false, 480 | "inputs": [{ "indexed": false, "name": "quorum", "type": "uint256" }], 481 | "name": "QuorumChanged", 482 | "type": "event" 483 | }, 484 | { 485 | "anonymous": false, 486 | "inputs": [{ "indexed": false, "name": "epochId", "type": "uint256" }], 487 | "name": "ExpectedEpochIdUpdated", 488 | "type": "event" 489 | }, 490 | { 491 | "anonymous": false, 492 | "inputs": [ 493 | { "indexed": false, "name": "epochsPerFrame", "type": "uint64" }, 494 | { "indexed": false, "name": "slotsPerEpoch", "type": "uint64" }, 495 | { "indexed": false, "name": "secondsPerSlot", "type": "uint64" }, 496 | { "indexed": false, "name": "genesisTime", "type": "uint64" } 497 | ], 498 | "name": "BeaconSpecSet", 499 | "type": "event" 500 | }, 501 | { 502 | "anonymous": false, 503 | "inputs": [ 504 | { "indexed": false, "name": "epochId", "type": "uint256" }, 505 | { "indexed": false, "name": "beaconBalance", "type": "uint128" }, 506 | { "indexed": false, "name": "beaconValidators", "type": "uint128" }, 507 | { "indexed": false, "name": "caller", "type": "address" } 508 | ], 509 | "name": "BeaconReported", 510 | "type": "event" 511 | }, 512 | { 513 | "anonymous": false, 514 | "inputs": [ 515 | { "indexed": false, "name": "epochId", "type": "uint256" }, 516 | { "indexed": false, "name": "beaconBalance", "type": "uint128" }, 517 | { "indexed": false, "name": "beaconValidators", "type": "uint128" } 518 | ], 519 | "name": "Completed", 520 | "type": "event" 521 | }, 522 | { 523 | "anonymous": false, 524 | "inputs": [ 525 | { "indexed": false, "name": "postTotalPooledEther", "type": "uint256" }, 526 | { "indexed": false, "name": "preTotalPooledEther", "type": "uint256" }, 527 | { "indexed": false, "name": "timeElapsed", "type": "uint256" }, 528 | { "indexed": false, "name": "totalShares", "type": "uint256" } 529 | ], 530 | "name": "PostTotalShares", 531 | "type": "event" 532 | }, 533 | { 534 | "anonymous": false, 535 | "inputs": [{ "indexed": false, "name": "version", "type": "uint256" }], 536 | "name": "ContractVersionSet", 537 | "type": "event" 538 | } 539 | ] 540 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exactly/liquidation-bot", 3 | "version": "0.1.20", 4 | "private": true, 5 | "scripts": { 6 | "version": "changeset version && node .changeset/cargo.js && npm install --legacy-peer-deps --package-lock-only && git commit --all --amend --no-edit", 7 | "lint": "run-s --continue-on-error lint:**", 8 | "lint:eslint": "eslint --ext .ts,.js,.cjs,.mjs .", 9 | "lint:solhint": "solhint 'contracts/**/*.sol'", 10 | "lint:slither": "slither ." 11 | }, 12 | "engines": { 13 | "node": ">=16" 14 | }, 15 | "dependencies": { 16 | "@chainlink/contracts": "0.6.1", 17 | "@exactly/protocol": "^0.2.10", 18 | "@uniswap/v3-core": "1.0.1", 19 | "@uniswap/v3-periphery": "1.4.3", 20 | "solmate": "transmissions11/solmate#v7" 21 | }, 22 | "devDependencies": { 23 | "@changesets/cli": "^2.26.1", 24 | "@changesets/types": "^5.2.1", 25 | "@ltd/j-toml": "^1.38.0", 26 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", 27 | "@tenderly/hardhat-tenderly": "^1.6.1", 28 | "@typechain/ethers-v5": "^10.2.0", 29 | "@typechain/hardhat": "^6.1.5", 30 | "@types/eslint": "^8.37.0", 31 | "@types/node": "^18.15.11", 32 | "@typescript-eslint/eslint-plugin": "^5.57.1", 33 | "@typescript-eslint/parser": "^5.57.1", 34 | "dotenv": "^16.0.3", 35 | "eslint": "^8.37.0", 36 | "eslint-config-prettier": "^8.8.0", 37 | "eslint-import-resolver-typescript": "^3.5.4", 38 | "eslint-plugin-eslint-comments": "^3.2.0", 39 | "eslint-plugin-import": "^2.27.5", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "ethers": "^6.2.3", 43 | "hardhat": "2.13.0", 44 | "hardhat-deploy": "^0.11.25", 45 | "npm-run-all": "^4.1.5", 46 | "prettier": "^2.8.7", 47 | "prettier-plugin-solidity": "^1.1.3", 48 | "solhint": "^3.4.1", 49 | "solhint-plugin-prettier": "^0.0.5", 50 | "ts-node": "^10.9.1", 51 | "typechain": "^8.1.1", 52 | "typescript": "^5.0.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @exactly-protocol/=node_modules/@exactly-protocol/ 2 | @chainlink/=node_modules/@chainlink/ 3 | @openzeppelin/=node_modules/@openzeppelin/ 4 | @uniswap/=node_modules/@uniswap/ 5 | forge-std/=lib/forge-std/src/ 6 | solmate/=node_modules/solmate/ 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | slither-analyzer~=0.9.3 2 | solc-select~=1.0.3 3 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "solc-version,unused-return,missing-zero-check", 3 | "filter_paths": "node_modules|mocks" 4 | } 5 | -------------------------------------------------------------------------------- /src/account.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use std::{ 5 | collections::HashMap, 6 | fmt::{Debug, Formatter}, 7 | }; 8 | 9 | use crate::{ 10 | fixed_point_math::{FixedPointMath, FixedPointMathGen}, 11 | generate_abi::{ 12 | BorrowAtMaturityFilter, DepositAtMaturityFilter, RepayAtMaturityFilter, 13 | WithdrawAtMaturityFilter, 14 | }, 15 | Market, 16 | }; 17 | 18 | #[derive(Serialize, Deserialize, Clone, Default, Eq, PartialEq)] 19 | pub struct AccountPosition { 20 | pub fixed_deposit_positions: HashMap, 21 | pub fixed_borrow_positions: HashMap, 22 | pub floating_deposit_shares: U256, 23 | pub floating_borrow_shares: U256, 24 | pub is_collateral: bool, 25 | } 26 | 27 | impl Debug for AccountPosition { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 29 | // let market = self.market.lock().unwrap(); 30 | write!( 31 | f, 32 | " 33 | \t\tfloating_deposit_shares : {:#?} 34 | \t\tfloating_borrow_shares : {:#?} 35 | \t\tfixed_deposit_positions : {:#?} 36 | \t\tfixed_borrow_positions : {:#?} 37 | ", 38 | self.floating_deposit_shares, 39 | self.floating_borrow_shares, 40 | self.fixed_deposit_positions, 41 | self.fixed_borrow_positions, 42 | ) 43 | } 44 | } 45 | 46 | impl AccountPosition { 47 | pub fn new() -> Self { 48 | Default::default() 49 | } 50 | 51 | pub fn floating_deposit_assets( 52 | &self, 53 | market: &Market, 54 | timestamp: U256, 55 | ) -> U256 { 56 | if market.floating_deposit_shares == U256::zero() { 57 | self.floating_deposit_shares 58 | } else { 59 | self.floating_deposit_shares.mul_div_down( 60 | market.total_assets(timestamp), 61 | market.floating_deposit_shares, 62 | ) 63 | } 64 | } 65 | 66 | pub fn floating_borrow_assets( 67 | &self, 68 | market: &Market, 69 | timestamp: U256, 70 | ) -> U256 { 71 | if market.floating_borrow_shares == U256::zero() { 72 | self.floating_borrow_shares 73 | } else { 74 | self.floating_borrow_shares.mul_div_up( 75 | market.total_floating_borrow_assets(timestamp), 76 | market.floating_borrow_shares, 77 | ) 78 | } 79 | } 80 | } 81 | 82 | #[derive(Eq, Clone, Default, Serialize, Deserialize)] 83 | pub struct Account { 84 | pub address: Address, 85 | pub positions: HashMap, 86 | } 87 | 88 | impl PartialEq for Account { 89 | fn eq(&self, other: &Self) -> bool { 90 | self.address == other.address 91 | } 92 | } 93 | 94 | impl Debug for Account { 95 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 96 | write!( 97 | f, 98 | "\n============== 99 | \tAccount {:?} 100 | \tData\n{:?}\n", 101 | self.address, self.positions 102 | ) 103 | } 104 | } 105 | 106 | impl Account { 107 | pub fn new(address: Address, market_map: &HashMap) -> Self { 108 | let mut markets = HashMap::::new(); 109 | for address in market_map.keys() { 110 | markets.insert(*address, AccountPosition::new()); 111 | } 112 | 113 | Self { 114 | address, 115 | positions: markets, 116 | } 117 | } 118 | 119 | pub fn deposit_at_maturity(&mut self, deposit: &DepositAtMaturityFilter, market: &Address) { 120 | let data = self.positions.entry(*market).or_default(); 121 | let supply = data 122 | .fixed_deposit_positions 123 | .entry(deposit.maturity) 124 | .or_default(); 125 | *supply += deposit.assets + deposit.fee; 126 | } 127 | 128 | pub fn withdraw_at_maturity(&mut self, withdraw: WithdrawAtMaturityFilter, market: &Address) { 129 | let data = self.positions.entry(*market).or_default(); 130 | if data 131 | .fixed_deposit_positions 132 | .contains_key(&withdraw.maturity) 133 | { 134 | let supply = data 135 | .fixed_deposit_positions 136 | .get_mut(&withdraw.maturity) 137 | .unwrap(); 138 | // TODO check if this is correct 139 | *supply -= withdraw.position_assets; 140 | } 141 | } 142 | 143 | pub fn borrow_at_maturity(&mut self, borrow: &BorrowAtMaturityFilter, market: &Address) { 144 | let data = self.positions.entry(*market).or_default(); 145 | 146 | let borrowed = data 147 | .fixed_borrow_positions 148 | .entry(borrow.maturity) 149 | .or_default(); 150 | *borrowed += borrow.assets + borrow.fee; 151 | } 152 | 153 | pub fn repay_at_maturity(&mut self, repay: &RepayAtMaturityFilter, market: &Address) { 154 | let data = self.positions.entry(*market).or_default(); 155 | if let Some(position) = data.fixed_borrow_positions.get_mut(&repay.maturity) { 156 | *position -= repay.position_assets; 157 | } 158 | } 159 | 160 | pub fn set_collateral(&mut self, market: &Address) { 161 | let data = self.positions.entry(*market).or_default(); 162 | data.is_collateral = true; 163 | } 164 | 165 | pub fn unset_collateral(&mut self, market: &Address) { 166 | let data = self.positions.entry(*market).or_default(); 167 | data.is_collateral = false; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | extern crate dotenv; 2 | 3 | use ethers::prelude::{coins_bip39::English, k256::ecdsa::SigningKey, MnemonicBuilder, Wallet}; 4 | use ethers::types::U256; 5 | use ethers::utils::{self, ParseUnits}; 6 | use std::collections::HashMap; 7 | use std::env; 8 | use std::fmt::Debug; 9 | use std::sync::Arc; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Config { 13 | pub chain_id: u64, 14 | pub chain_id_name: String, 15 | pub wallet: Wallet, 16 | pub rpc_provider: String, 17 | pub rpc_provider_relayer: String, 18 | pub comparison_enabled: bool, 19 | pub token_pairs: String, 20 | pub backup: u32, 21 | pub liquidate_unprofitable: bool, 22 | pub repay_offset: U256, 23 | pub sentry_dsn: Option, 24 | pub simulation: bool, 25 | pub adjust_factor: HashMap, 26 | pub gas_price: U256, 27 | pub gas_used: U256, 28 | pub l1_gas_price: U256, 29 | pub l1_gas_used: U256, 30 | pub page_size: u64, 31 | } 32 | 33 | impl Default for Config { 34 | fn default() -> Self { 35 | dotenv::from_filename(".env").ok(); 36 | let chain_id = get_env_or_throw("CHAIN_ID") 37 | .parse::() 38 | .expect("CHAIN_ID is not number"); 39 | 40 | let (chain_id_name, rpc_provider, rpc_provider_relayer) = match chain_id { 41 | 1 => { 42 | dotenv::from_filename(".env.mainnet").ok(); 43 | ( 44 | "mainnet", 45 | get_env_or_throw("MAINNET_NODE"), 46 | get_env_or_throw("MAINNET_NODE_RELAYER"), 47 | ) 48 | } 49 | 5 => { 50 | dotenv::from_filename(".env.goerli").ok(); 51 | ( 52 | "goerli", 53 | get_env_or_throw("GOERLI_NODE"), 54 | get_env_or_throw("GOERLI_NODE_RELAYER"), 55 | ) 56 | } 57 | 10 => { 58 | dotenv::from_filename(".env.optimism").ok(); 59 | ( 60 | "optimism", 61 | get_env_or_throw("OPTIMISM_NODE"), 62 | get_env_or_throw("OPTIMISM_NODE_RELAYER"), 63 | ) 64 | } 65 | 1337 => ( 66 | "fork", 67 | get_env_or_throw("FORK_NODE"), 68 | get_env_or_throw("FORK_NODE_RELAYER"), 69 | ), 70 | _ => { 71 | panic!("Unknown network!") 72 | } 73 | }; 74 | 75 | let wallet = MnemonicBuilder::::default() 76 | .phrase(env::var("MNEMONIC").unwrap().as_str()) 77 | .build() 78 | .unwrap(); 79 | 80 | let comparison_enabled: bool = env::var("COMPARISON_ENABLED") 81 | .unwrap_or_else(|_| "parse".into()) 82 | .parse::() 83 | .unwrap_or(false); 84 | 85 | let backup = env::var("BACKUP") 86 | .unwrap_or_else(|_| "0".into()) 87 | .parse::() 88 | .unwrap_or(0); 89 | 90 | let token_pairs = env::var("TOKEN_PAIRS").unwrap_or_else(|_| "".into()); 91 | 92 | let simulation: bool = env::var("SIMULATION") 93 | .unwrap_or_else(|_| "false".into()) 94 | .parse::() 95 | .unwrap_or(false); 96 | 97 | let adjust_factor = env::var("ADJUST_FACTOR").unwrap_or_else(|_| "".into()); 98 | let adjust_factor: HashMap = 99 | serde_json::from_str::>(&adjust_factor) 100 | .map(|x| { 101 | x.into_iter() 102 | .map(|(k, v)| { 103 | ( 104 | "exa".to_string() + &k, 105 | U256::from_dec_str(&v).unwrap_or(U256::zero()), 106 | ) 107 | }) 108 | .collect() 109 | }) 110 | .unwrap_or_default(); 111 | 112 | let repay_offset = utils::parse_units( 113 | env::var("REPAY_OFFSET").unwrap_or_else(|_| "0.001".into()), 114 | 18, 115 | ) 116 | .unwrap(); 117 | let repay_offset = match repay_offset { 118 | ParseUnits::U256(repay_offset) => repay_offset, 119 | _ => U256::from(0), 120 | }; 121 | 122 | let liquidate_unprofitable = 123 | Arc::new(env::var("LIQUIDATE_UNPROFITABLE").unwrap_or_else(|_| "false".into())) 124 | .parse::() 125 | .unwrap_or(false); 126 | 127 | let sentry_dsn = env::var("SENTRY_DSN").ok(); 128 | 129 | let gas_price = U256::from_dec_str(&env::var("GAS_PRICE").expect("GAS_PRICE is not set")) 130 | .expect("GAS_PRICE is not number"); 131 | 132 | let gas_used = U256::from_dec_str(&env::var("GAS_USED").expect("GAS_USED is not set")) 133 | .expect("GAS_USED is not number"); 134 | 135 | let l1_gas_price = U256::from_dec_str(&env::var("L1_GAS_PRICE").unwrap_or_else(|_| { 136 | if chain_id != 10 { 137 | "0".to_string() 138 | } else { 139 | panic!("L1_GAS_PRICE is not set") 140 | } 141 | })) 142 | .expect("L1_GAS_PRICE is not number"); 143 | 144 | let l1_gas_used = U256::from_dec_str(&env::var("L1_GAS_USED").unwrap_or_else(|_| { 145 | if chain_id != 10 { 146 | "0".to_string() 147 | } else { 148 | panic!("L1_GAS_USED is not set") 149 | } 150 | })) 151 | .expect("L1_GAS_USED is not number"); 152 | 153 | let page_size = env::var("PAGE_SIZE") 154 | .unwrap_or_else(|_| "5000".into()) 155 | .parse::() 156 | .unwrap_or(100); 157 | 158 | Config { 159 | chain_id, 160 | chain_id_name: chain_id_name.into(), 161 | wallet, 162 | rpc_provider, 163 | rpc_provider_relayer, 164 | comparison_enabled, 165 | token_pairs, 166 | backup, 167 | liquidate_unprofitable, 168 | repay_offset, 169 | sentry_dsn, 170 | simulation, 171 | adjust_factor, 172 | gas_price, 173 | gas_used, 174 | l1_gas_price, 175 | l1_gas_used, 176 | page_size, 177 | } 178 | } 179 | } 180 | 181 | fn get_env_or_throw(env: &str) -> String { 182 | env::var(env).unwrap_or_else(|_| panic!("No {}", env)) 183 | } 184 | -------------------------------------------------------------------------------- /src/exactly_events.rs: -------------------------------------------------------------------------------- 1 | use crate::generate_abi::lido_oracle::RecoverToVaultFilter; 2 | use crate::generate_abi::lido_oracle::ScriptResultFilter; 3 | use crate::generate_abi::market_protocol::ApprovalFilter; 4 | use crate::generate_abi::market_protocol::TransferFilter; 5 | use crate::generate_abi::AllowedBeaconBalanceAnnualRelativeIncreaseSetFilter; 6 | use crate::generate_abi::AllowedBeaconBalanceRelativeDecreaseSetFilter; 7 | use crate::generate_abi::BeaconReportReceiverSetFilter; 8 | use crate::generate_abi::BeaconReportedFilter; 9 | use crate::generate_abi::BeaconSpecSetFilter; 10 | use crate::generate_abi::CompletedFilter; 11 | use crate::generate_abi::ContractVersionSetFilter; 12 | use crate::generate_abi::ExpectedEpochIdUpdatedFilter; 13 | use crate::generate_abi::MemberAddedFilter; 14 | use crate::generate_abi::MemberRemovedFilter; 15 | use crate::generate_abi::PostTotalSharesFilter; 16 | use crate::generate_abi::QuorumChangedFilter; 17 | use crate::generate_abi::RewardsControllerSetFilter; 18 | use crate::generate_abi::{ 19 | auditor::{ 20 | AdminChangedFilter, InitializedFilter, RoleAdminChangedFilter, RoleGrantedFilter, 21 | RoleRevokedFilter, UpgradedFilter, 22 | }, 23 | price_feed::{AnswerUpdatedFilter, NewRoundFilter}, 24 | AccumulatorAccrualFilter, AdjustFactorSetFilter, BackupFeeRateSetFilter, 25 | BorrowAtMaturityFilter, BorrowFilter, DampSpeedSetFilter, DepositAtMaturityFilter, 26 | DepositFilter, EarningsAccumulatorSmoothFactorSetFilter, FixedEarningsUpdateFilter, 27 | FloatingDebtUpdateFilter, InterestRateModelSetFilter, LiquidateFilter, 28 | LiquidationIncentiveSetFilter, MarketEnteredFilter, MarketExitedFilter, MarketListedFilter, 29 | MarketUpdateFilter, MaxFuturePoolsSetFilter, PausedFilter, PenaltyRateSetFilter, 30 | PriceFeedSetFilter, RepayAtMaturityFilter, RepayFilter, ReserveFactorSetFilter, SeizeFilter, 31 | TreasurySetFilter, UnpausedFilter, WithdrawAtMaturityFilter, WithdrawFilter, 32 | }; 33 | use aggregator_mod::NewTransmissionFilter; 34 | use ethers::{ 35 | abi::{Error, RawLog}, 36 | prelude::EthLogDecode, 37 | types::H256, 38 | }; 39 | use sentry::Breadcrumb; 40 | use sentry::Level; 41 | use serde_json::Value; 42 | use std::collections::BTreeMap; 43 | use std::str::FromStr; 44 | 45 | #[derive(Debug, Clone, PartialEq, Eq)] 46 | pub enum ExactlyEvents { 47 | Transfer(TransferFilter), 48 | Deposit(DepositFilter), 49 | Withdraw(WithdrawFilter), 50 | Approval(ApprovalFilter), 51 | DepositAtMaturity(DepositAtMaturityFilter), 52 | WithdrawAtMaturity(WithdrawAtMaturityFilter), 53 | BorrowAtMaturity(BorrowAtMaturityFilter), 54 | RepayAtMaturity(RepayAtMaturityFilter), 55 | Liquidate(LiquidateFilter), 56 | Seize(SeizeFilter), 57 | EarningsAccumulatorSmoothFactorSet(EarningsAccumulatorSmoothFactorSetFilter), 58 | MaxFuturePoolsSet(MaxFuturePoolsSetFilter), 59 | TreasurySet(TreasurySetFilter), 60 | RoleGranted(RoleGrantedFilter), 61 | RoleAdminChanged(RoleAdminChangedFilter), 62 | RoleRevoked(RoleRevokedFilter), 63 | Paused(PausedFilter), 64 | Unpaused(UnpausedFilter), 65 | MarketUpdate(MarketUpdateFilter), 66 | FixedEarningsUpdate(FixedEarningsUpdateFilter), 67 | AccumulatorAccrual(AccumulatorAccrualFilter), 68 | FloatingDebtUpdate(FloatingDebtUpdateFilter), 69 | Borrow(BorrowFilter), 70 | Repay(RepayFilter), 71 | BackupFeeRateSet(BackupFeeRateSetFilter), 72 | 73 | // Auditor events 74 | MarketListed(MarketListedFilter), 75 | MarketEntered(MarketEnteredFilter), 76 | MarketExited(MarketExitedFilter), 77 | LiquidationIncentiveSet(LiquidationIncentiveSetFilter), 78 | AdjustFactorSet(AdjustFactorSetFilter), 79 | Upgraded(UpgradedFilter), 80 | Initialized(InitializedFilter), 81 | AdminChanged(AdminChangedFilter), 82 | 83 | // PoolAccounting events 84 | InterestRateModelSet(InterestRateModelSetFilter), 85 | PenaltyRateSet(PenaltyRateSetFilter), 86 | ReserveFactorSet(ReserveFactorSetFilter), 87 | DampSpeedSet(DampSpeedSetFilter), 88 | 89 | // ExactlyOracle events 90 | PriceFeedSetFilter(PriceFeedSetFilter), 91 | // PriceFeed 92 | AnswerUpdated(AnswerUpdatedFilter), 93 | NewRound(NewRoundFilter), 94 | NewTransmission(NewTransmissionFilter), 95 | PostTotalShares(PostTotalSharesFilter), 96 | 97 | UpdateLidoPrice(Option), 98 | 99 | Ignore(Option), 100 | } 101 | 102 | macro_rules! map_filter { 103 | ($ext_filter:ident, $exactly_filter:expr, $log:ident) => { 104 | if let Ok(_) = $ext_filter::decode_log($log) { 105 | return Ok($exactly_filter); 106 | } 107 | }; 108 | } 109 | 110 | impl EthLogDecode for ExactlyEvents { 111 | fn decode_log(log: &RawLog) -> Result 112 | where 113 | Self: Sized, 114 | { 115 | if let Ok(decoded) = RoleGrantedFilter::decode_log(log) { 116 | return Ok(ExactlyEvents::RoleGranted(decoded)); 117 | } 118 | if let Ok(decoded) = RoleAdminChangedFilter::decode_log(log) { 119 | return Ok(ExactlyEvents::RoleAdminChanged(decoded)); 120 | } 121 | if let Ok(decoded) = RoleRevokedFilter::decode_log(log) { 122 | return Ok(ExactlyEvents::RoleRevoked(decoded)); 123 | } 124 | if let Ok(decoded) = TransferFilter::decode_log(log) { 125 | return Ok(ExactlyEvents::Transfer(decoded)); 126 | } 127 | if let Ok(decoded) = DepositFilter::decode_log(log) { 128 | return Ok(ExactlyEvents::Deposit(decoded)); 129 | } 130 | if let Ok(decoded) = WithdrawFilter::decode_log(log) { 131 | return Ok(ExactlyEvents::Withdraw(decoded)); 132 | } 133 | if let Ok(decoded) = ApprovalFilter::decode_log(log) { 134 | return Ok(ExactlyEvents::Approval(decoded)); 135 | } 136 | if let Ok(decoded) = DepositAtMaturityFilter::decode_log(log) { 137 | return Ok(ExactlyEvents::DepositAtMaturity(decoded)); 138 | } 139 | if let Ok(decoded) = WithdrawAtMaturityFilter::decode_log(log) { 140 | return Ok(ExactlyEvents::WithdrawAtMaturity(decoded)); 141 | } 142 | if let Ok(decoded) = BorrowAtMaturityFilter::decode_log(log) { 143 | return Ok(ExactlyEvents::BorrowAtMaturity(decoded)); 144 | } 145 | if let Ok(decoded) = RepayAtMaturityFilter::decode_log(log) { 146 | return Ok(ExactlyEvents::RepayAtMaturity(decoded)); 147 | } 148 | if let Ok(decoded) = LiquidateFilter::decode_log(log) { 149 | return Ok(ExactlyEvents::Liquidate(decoded)); 150 | } 151 | if let Ok(decoded) = SeizeFilter::decode_log(log) { 152 | return Ok(ExactlyEvents::Seize(decoded)); 153 | } 154 | if let Ok(decoded) = EarningsAccumulatorSmoothFactorSetFilter::decode_log(log) { 155 | return Ok(ExactlyEvents::EarningsAccumulatorSmoothFactorSet(decoded)); 156 | } 157 | if let Ok(decoded) = MaxFuturePoolsSetFilter::decode_log(log) { 158 | return Ok(ExactlyEvents::MaxFuturePoolsSet(decoded)); 159 | } 160 | if let Ok(decoded) = PausedFilter::decode_log(log) { 161 | return Ok(ExactlyEvents::Paused(decoded)); 162 | } 163 | if let Ok(decoded) = UnpausedFilter::decode_log(log) { 164 | return Ok(ExactlyEvents::Unpaused(decoded)); 165 | } 166 | if let Ok(decoded) = MarketUpdateFilter::decode_log(log) { 167 | return Ok(ExactlyEvents::MarketUpdate(decoded)); 168 | } 169 | if let Ok(decoded) = FixedEarningsUpdateFilter::decode_log(log) { 170 | return Ok(ExactlyEvents::FixedEarningsUpdate(decoded)); 171 | } 172 | if let Ok(decoded) = AccumulatorAccrualFilter::decode_log(log) { 173 | return Ok(ExactlyEvents::AccumulatorAccrual(decoded)); 174 | } 175 | if let Ok(decoded) = FloatingDebtUpdateFilter::decode_log(log) { 176 | return Ok(ExactlyEvents::FloatingDebtUpdate(decoded)); 177 | } 178 | if let Ok(decoded) = TreasurySetFilter::decode_log(log) { 179 | return Ok(ExactlyEvents::TreasurySet(decoded)); 180 | } 181 | if let Ok(decoded) = BorrowFilter::decode_log(log) { 182 | return Ok(ExactlyEvents::Borrow(decoded)); 183 | } 184 | if let Ok(decoded) = RepayFilter::decode_log(log) { 185 | return Ok(ExactlyEvents::Repay(decoded)); 186 | } 187 | if let Ok(decoded) = BackupFeeRateSetFilter::decode_log(log) { 188 | return Ok(ExactlyEvents::BackupFeeRateSet(decoded)); 189 | } 190 | 191 | // Auditor events 192 | if let Ok(decoded) = MarketListedFilter::decode_log(log) { 193 | return Ok(ExactlyEvents::MarketListed(decoded)); 194 | } 195 | if let Ok(decoded) = MarketEnteredFilter::decode_log(log) { 196 | return Ok(ExactlyEvents::MarketEntered(decoded)); 197 | } 198 | if let Ok(decoded) = MarketExitedFilter::decode_log(log) { 199 | return Ok(ExactlyEvents::MarketExited(decoded)); 200 | } 201 | if let Ok(decoded) = LiquidationIncentiveSetFilter::decode_log(log) { 202 | return Ok(ExactlyEvents::LiquidationIncentiveSet(decoded)); 203 | } 204 | if let Ok(decoded) = AdjustFactorSetFilter::decode_log(log) { 205 | return Ok(ExactlyEvents::AdjustFactorSet(decoded)); 206 | } 207 | if let Ok(decoded) = AdminChangedFilter::decode_log(log) { 208 | return Ok(ExactlyEvents::AdminChanged(decoded)); 209 | } 210 | if let Ok(decoded) = UpgradedFilter::decode_log(log) { 211 | return Ok(ExactlyEvents::Upgraded(decoded)); 212 | } 213 | if let Ok(decoded) = InitializedFilter::decode_log(log) { 214 | return Ok(ExactlyEvents::Initialized(decoded)); 215 | } 216 | 217 | // PoolAccounting events 218 | if let Ok(decoded) = InterestRateModelSetFilter::decode_log(log) { 219 | return Ok(ExactlyEvents::InterestRateModelSet(decoded)); 220 | } 221 | if let Ok(decoded) = PenaltyRateSetFilter::decode_log(log) { 222 | return Ok(ExactlyEvents::PenaltyRateSet(decoded)); 223 | } 224 | if let Ok(decoded) = ReserveFactorSetFilter::decode_log(log) { 225 | return Ok(ExactlyEvents::ReserveFactorSet(decoded)); 226 | } 227 | if let Ok(decoded) = DampSpeedSetFilter::decode_log(log) { 228 | return Ok(ExactlyEvents::DampSpeedSet(decoded)); 229 | } 230 | 231 | // ExactlyOracle events 232 | if let Ok(decoded) = PriceFeedSetFilter::decode_log(log) { 233 | return Ok(ExactlyEvents::PriceFeedSetFilter(decoded)); 234 | } 235 | 236 | // PriceFeed 237 | if let Ok(decoded) = AnswerUpdatedFilter::decode_log(log) { 238 | return Ok(ExactlyEvents::AnswerUpdated(decoded)); 239 | } 240 | 241 | if let Ok(decoded) = NewRoundFilter::decode_log(log) { 242 | return Ok(ExactlyEvents::NewRound(decoded)); 243 | } 244 | 245 | if let Ok(decoded) = NewTransmissionFilter::decode_log(log) { 246 | return Ok(ExactlyEvents::NewTransmission(decoded)); 247 | } 248 | 249 | if let Ok(decoded) = PostTotalSharesFilter::decode_log(log) { 250 | return Ok(ExactlyEvents::PostTotalShares(decoded)); 251 | } 252 | 253 | let exactly_event = ExactlyEvents::Ignore(log.topics.first().copied()); 254 | map_filter!( 255 | AllowedBeaconBalanceAnnualRelativeIncreaseSetFilter, 256 | exactly_event, 257 | log 258 | ); 259 | map_filter!( 260 | AllowedBeaconBalanceRelativeDecreaseSetFilter, 261 | exactly_event, 262 | log 263 | ); 264 | map_filter!(BeaconReportReceiverSetFilter, exactly_event, log); 265 | map_filter!(BeaconReportedFilter, exactly_event, log); 266 | map_filter!(BeaconSpecSetFilter, exactly_event, log); 267 | map_filter!(CompletedFilter, exactly_event, log); 268 | map_filter!(ContractVersionSetFilter, exactly_event, log); 269 | map_filter!(ExpectedEpochIdUpdatedFilter, exactly_event, log); 270 | map_filter!(MemberAddedFilter, exactly_event, log); 271 | map_filter!(MemberRemovedFilter, exactly_event, log); 272 | map_filter!(PostTotalSharesFilter, exactly_event, log); 273 | map_filter!(QuorumChangedFilter, exactly_event, log); 274 | map_filter!(RecoverToVaultFilter, exactly_event, log); 275 | map_filter!(ScriptResultFilter, exactly_event, log); 276 | map_filter!(RewardsControllerSetFilter, exactly_event, log); 277 | 278 | let ignored_events: Vec = [ 279 | "0xe8ec50e5150ae28ae37e493ff389ffab7ffaec2dc4dccfca03f12a3de29d12b2", 280 | "0xd0d9486a2c673e2a4b57fc82e4c8a556b3e2b82dd5db07e2c04a920ca0f469b6", 281 | "0xd0b1dac935d85bd54cf0a33b0d41d39f8cf53a968465fc7ea2377526b8ac712c", 282 | "0x25d719d88a4512dd76c7442b910a83360845505894eb444ef299409e180f8fb9", // ConfigSet(uint32,uint64,address[],address[],uint8,uint64,bytes) 283 | "0x3ea16a923ff4b1df6526e854c9e3a995c43385d70e73359e10623c74f0b52037", // RoundRequested(address,bytes16,uint32,uint8) 284 | "0x78af32efdcad432315431e9b03d27e6cd98fb79c405fdc5af7c1714d9c0f75b3", // PayeeshipTransferred(address,address,address) 285 | "0xed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae1278", // OwnershipTransferRequested(address,address) 286 | "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", // OwnershipTransferred(address,address) 287 | "0x84f7c7c80bb8ed2279b4aab5f61cd05e6374073d38f46d7f32de8c30e9e38367", // PayeeshipTransferRequested(address,address,address) 288 | ] 289 | .iter() 290 | .map(|x| H256::from_str(x).unwrap()) 291 | .collect(); 292 | if log.topics.iter().any(|topic| { 293 | ignored_events 294 | .iter() 295 | .any(|ignored_topic| topic == ignored_topic) 296 | }) { 297 | return Ok(ExactlyEvents::Ignore(log.topics.first().copied())); 298 | }; 299 | 300 | missing_event_breadcrumb(log); 301 | panic!("Missing event {:?}", log.topics.first().copied()); 302 | } 303 | } 304 | 305 | fn missing_event_breadcrumb(log: &RawLog) { 306 | let mut data = BTreeMap::new(); 307 | data.insert( 308 | "data".to_string(), 309 | Value::String(hex::encode(log.data.clone())), 310 | ); 311 | data.insert( 312 | "topics".to_string(), 313 | Value::Array( 314 | log.topics 315 | .iter() 316 | .map(|t| Value::String(format!("{:X}", t))) 317 | .collect(), 318 | ), 319 | ); 320 | sentry::add_breadcrumb(Breadcrumb { 321 | ty: "error".to_string(), 322 | category: Some("Missing event".to_string()), 323 | level: Level::Error, 324 | data, 325 | ..Default::default() 326 | }); 327 | } 328 | 329 | mod aggregator_mod { 330 | use ethers::{ 331 | prelude::{EthDisplay, EthEvent}, 332 | types::{Address, Bytes, I256}, 333 | }; 334 | 335 | #[derive( 336 | Clone, 337 | Debug, 338 | Default, 339 | Eq, 340 | PartialEq, 341 | EthEvent, 342 | EthDisplay, 343 | serde::Deserialize, 344 | serde::Serialize, 345 | )] 346 | #[ethevent( 347 | name = "NewTransmission", 348 | abi = "NewTransmission(uint32,int192,address,int192[],bytes,bytes32)" 349 | )] 350 | pub struct NewTransmissionFilter { 351 | #[ethevent(indexed)] 352 | pub aggregator_round_id: u32, 353 | pub answer: I256, 354 | pub transmitter: Address, 355 | pub observations: Vec, 356 | pub observers: Bytes, 357 | pub raw_report_context: [u8; 32], 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/fixed_point_math.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::{I256, U256}; 2 | 3 | pub mod math { 4 | use ethers::types::U256; 5 | 6 | pub const WAD: U256 = make_u256(1_000_000_000_000_000_000u64); 7 | pub const RECOVERY_THRESHOLD: U256 = WAD; 8 | 9 | pub const fn make_u256(x: u64) -> U256 { 10 | U256([x, 0, 0, 0]) 11 | } 12 | } 13 | pub trait FixedPointMathGen { 14 | fn mul_div_down(&self, y: T, denominator: T) -> T; 15 | } 16 | pub trait FixedPointMath { 17 | fn mul_div_up(&self, y: Self, denominator: Self) -> Self; 18 | fn mul_wad_down(&self, y: Self) -> Self; 19 | fn mul_wad_up(&self, y: Self) -> Self; 20 | fn div_wad_down(&self, y: Self) -> Self; 21 | fn div_wad_up(&self, y: Self) -> Self; 22 | fn ln_wad(&self) -> I256; 23 | } 24 | 25 | #[inline(always)] 26 | fn lt(x: U256, y: U256) -> U256 { 27 | if x < y { 28 | U256::from(1u32) 29 | } else { 30 | U256::zero() 31 | } 32 | } 33 | 34 | fn log2(x: U256) -> U256 { 35 | let mut r; 36 | r = lt(U256::from(0xffffffffffffffffffffffffffffffffu128), x) << 7u32; 37 | r = r | (lt(U256::from(0xffffffffffffffffu128), x >> r) << 6u32); 38 | r = r | (lt(U256::from(0xffffffffu128), x >> r) << 5u32); 39 | r = r | (lt(U256::from(0xffffu128), x >> r) << 4u32); 40 | r = r | (lt(U256::from(0xffu128), x >> r) << 3u32); 41 | r = r | (lt(U256::from(0xfu128), x >> r) << 2u32); 42 | r = r | (lt(U256::from(0x3u128), x >> r) << 1u32); 43 | r = r | lt(U256::from(0x1u128), x >> r); 44 | r 45 | } 46 | 47 | impl FixedPointMathGen for U256 { 48 | fn mul_div_down(&self, y: I256, denominator: I256) -> I256 { 49 | let z = I256::from_raw(*self) * y; 50 | z / denominator 51 | } 52 | } 53 | 54 | impl FixedPointMathGen for U256 { 55 | fn mul_div_down(&self, y: Self, denominator: Self) -> Self { 56 | let z = self * y; 57 | z / denominator 58 | } 59 | } 60 | 61 | impl FixedPointMath for U256 { 62 | fn ln_wad(&self) -> I256 { 63 | let mut x = *self; 64 | let k = I256::from_raw(log2(x)) - I256::from(96u32); 65 | x <<= (I256::from(159u32) - k).into_raw(); 66 | let x = I256::from_raw(x >> 159u32); 67 | 68 | let mut p = x + I256::from(3273285459638523848632254066296u128); 69 | p = ((p * x) >> 96) + I256::from(24828157081833163892658089445524u128); 70 | p = ((p * x) >> 96) + I256::from(43456485725739037958740375743393u128); 71 | p = ((p * x) >> 96) - I256::from(11111509109440967052023855526967u128); 72 | p = ((p * x) >> 96) - I256::from(45023709667254063763336534515857u128); 73 | p = ((p * x) >> 96) - I256::from(14706773417378608786704636184526u128); 74 | p = p * x - (I256::from(795164235651350426258249787498u128) << 96); 75 | 76 | let mut q = x + I256::from(5573035233440673466300451813936u128); 77 | q = ((q * x) >> 96) + I256::from(71694874799317883764090561454958u128); 78 | q = ((q * x) >> 96) + I256::from(283447036172924575727196451306956u128); 79 | q = ((q * x) >> 96) + I256::from(401686690394027663651624208769553u128); 80 | q = ((q * x) >> 96) + I256::from(204048457590392012362485061816622u128); 81 | q = ((q * x) >> 96) + I256::from(31853899698501571402653359427138u128); 82 | q = ((q * x) >> 96) + I256::from(909429971244387300277376558375u128); 83 | 84 | let mut r = p / q; 85 | 86 | r *= I256::from_raw( 87 | U256::from_str_radix("1677202110996718588342820967067443963516166", 10).unwrap(), 88 | ); 89 | r += I256::from_raw( 90 | U256::from_str_radix( 91 | "16597577552685614221487285958193947469193820559219878177908093499208371", 92 | 10, 93 | ) 94 | .unwrap(), 95 | ) * k; 96 | r += I256::from_raw( 97 | U256::from_str_radix( 98 | "600920179829731861736702779321621459595472258049074101567377883020018308", 99 | 10, 100 | ) 101 | .unwrap(), 102 | ); 103 | 104 | r >>= 174; 105 | 106 | r 107 | } 108 | 109 | fn mul_wad_down(&self, y: Self) -> Self { 110 | self.mul_div_down(y, math::WAD) 111 | } 112 | 113 | fn mul_wad_up(&self, y: Self) -> Self { 114 | self.mul_div_up(y, math::WAD) 115 | } 116 | 117 | fn div_wad_down(&self, y: Self) -> Self { 118 | self.mul_div_down(math::WAD, y) 119 | } 120 | 121 | fn div_wad_up(&self, y: Self) -> Self { 122 | self.mul_div_up(math::WAD, y) 123 | } 124 | 125 | fn mul_div_up(&self, y: Self, denominator: Self) -> Self { 126 | let z: I256 = I256::from_raw(self * y); 127 | let m = if z.is_zero() { 128 | I256::zero() 129 | } else { 130 | I256::from(1u32) 131 | }; 132 | (m * ((z - I256::from(1u32)) / I256::from_raw(denominator) + I256::from(1u32))).into_raw() 133 | } 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | use super::*; 139 | use ethers::types::U256; 140 | 141 | #[test] 142 | fn test_ln_wad() { 143 | let a = U256::exp10(19); 144 | assert_eq!(a.ln_wad(), I256::from(2_302_585_092_994_045_683u128)); 145 | } 146 | 147 | #[test] 148 | fn test_log_2() { 149 | let a = U256::from(1024u32); 150 | assert_eq!(super::log2(a), U256::from(10u32)); 151 | } 152 | 153 | #[test] 154 | fn test_mul_div_up() { 155 | let a: U256 = U256::from(5u32); 156 | let r1 = a.mul_div_up(U256::from(2u32), U256::from(6u32)); 157 | let r2 = a.mul_div_up(U256::from(2u32), U256::from(5u32)); 158 | assert_eq!(r1, U256::from(2u32)); 159 | assert_eq!(r2, U256::from(2u32)); 160 | let r5 = U256::mul_div_up(&U256::from(4u32), U256::from(5u32), U256::from(3u32)); 161 | let r6 = U256::mul_div_up(&U256::from(4u32), U256::from(5u32), U256::from(10u32)); 162 | assert_eq!(r5, U256::from(7u32)); 163 | assert_eq!(r6, U256::from(2u32)); 164 | } 165 | 166 | #[test] 167 | fn test_mul_div_down() { 168 | let a: U256 = U256::from(5u32); 169 | let r1 = a.mul_div_down(U256::from(2u32), U256::from(6u32)); 170 | let r2 = a.mul_div_down(U256::from(2u32), U256::from(5u32)); 171 | assert_eq!(r1, U256::from(1u32)); 172 | assert_eq!(r2, U256::from(2u32)); 173 | let r5 = U256::mul_div_down(&U256::from(4u32), U256::from(5u32), U256::from(3u32)); 174 | let r6 = U256::mul_div_down(&U256::from(4u32), U256::from(5u32), U256::from(10u32)); 175 | assert_eq!(r5, U256::from(6u32)); 176 | assert_eq!(r6, U256::from(2u32)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/generate_abi.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::abigen; 2 | 3 | abigen!( 4 | MarketProtocol, 5 | "node_modules/@exactly/protocol/deployments/goerli/MarketWETH.json", 6 | event_derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | 9 | abigen!( 10 | Previewer, 11 | "node_modules/@exactly/protocol/deployments/goerli/Previewer.json", 12 | event_derives(serde::Deserialize, serde::Serialize) 13 | ); 14 | 15 | abigen!( 16 | PriceFeedWrapper, 17 | "node_modules/@exactly/protocol/deployments/goerli/PriceFeedwstETH.json", 18 | event_derives(serde::Deserialize, serde::Serialize) 19 | ); 20 | 21 | abigen!( 22 | Auditor, 23 | "node_modules/@exactly/protocol/deployments/goerli/Auditor.json", 24 | event_derives(serde::Deserialize, serde::Serialize) 25 | ); 26 | 27 | abigen!( 28 | InterestRateModel, 29 | "node_modules/@exactly/protocol/deployments/goerli/InterestRateModelWETH.json", 30 | event_derives(serde::Deserialize, serde::Serialize) 31 | ); 32 | 33 | abigen!( 34 | PriceFeed, 35 | "node_modules/@chainlink/contracts/abi/v0.8/AggregatorV2V3Interface.json", 36 | event_derives(serde::Deserialize, serde::Serialize) 37 | ); 38 | 39 | abigen!( 40 | PriceFeedDouble, 41 | "node_modules/@exactly/protocol/deployments/goerli/PriceFeedWBTC.json", 42 | event_derives(serde::Deserialize, serde::Serialize) 43 | ); 44 | 45 | abigen! { 46 | LidoOracle, 47 | "lib/abi/LidoOracle.json", 48 | event_derives(serde::Deserialize, serde::Serialize) 49 | } 50 | 51 | abigen!( 52 | PriceFeedLido, 53 | "lib/abi/Lido.json", 54 | event_derives(serde::Deserialize, serde::Serialize) 55 | ); 56 | 57 | abigen!( 58 | AggregatorProxy, 59 | "node_modules/@chainlink/contracts/abi/v0.7/AggregatorProxyInterface.json", 60 | event_derives(serde::Deserialize, serde::Serialize) 61 | ); 62 | 63 | abigen!( 64 | Liquidator, 65 | "deployments/goerli/Liquidator.json", 66 | event_derives(serde::Deserialize, serde::Serialize) 67 | ); 68 | -------------------------------------------------------------------------------- /src/liquidation.rs: -------------------------------------------------------------------------------- 1 | use super::config::Config; 2 | use super::fixed_point_math::{math, FixedPointMath, FixedPointMathGen}; 3 | use super::protocol::TokenPairFeeMap; 4 | use ethers::prelude::{ 5 | Address, Middleware, Multicall, MulticallVersion, Signer, SignerMiddleware, U256, 6 | }; 7 | use eyre::Result; 8 | use log::{error, info, warn}; 9 | use sentry::{Breadcrumb, Level}; 10 | use serde::Deserialize; 11 | use serde_json::Value; 12 | use std::cmp::Reverse; 13 | use std::collections::{BTreeMap, BinaryHeap, HashSet}; 14 | use std::str::FromStr; 15 | use std::sync::Arc; 16 | use std::time::SystemTime; 17 | use std::{collections::HashMap, time::Duration}; 18 | use tokio::sync::mpsc::Receiver; 19 | use tokio::sync::Mutex; 20 | use tokio::time; 21 | 22 | use super::{ 23 | protocol::{self, Protocol}, 24 | Account, 25 | }; 26 | use crate::generate_abi::{Auditor, LiquidationIncentive, Liquidator, MarketAccount, Previewer}; 27 | use crate::network::{Network, NetworkStatus}; 28 | 29 | #[derive(Default, Debug)] 30 | pub struct RepaySettings { 31 | pub max_repay: U256, 32 | pub pool_pair: Address, 33 | pub pair_fee: u32, 34 | pub fee: u32, 35 | } 36 | 37 | #[derive(Default, Debug)] 38 | pub struct Repay { 39 | pub price: U256, 40 | pub decimals: u8, 41 | pub market_to_seize: Option
, 42 | pub market_to_seize_value: U256, 43 | pub market_to_repay: Option
, 44 | pub market_to_liquidate_debt: U256, 45 | pub total_value_collateral: U256, 46 | pub total_adjusted_collateral: U256, 47 | pub total_value_debt: U256, 48 | pub total_adjusted_debt: U256, 49 | pub repay_asset_address: Address, 50 | pub collateral_asset_address: Address, 51 | } 52 | 53 | #[derive(Debug, Default)] 54 | pub enum LiquidationAction { 55 | #[default] 56 | Update, 57 | Insert, 58 | } 59 | 60 | #[derive(Default, Debug)] 61 | pub struct ProtocolState { 62 | pub gas_price: Option, 63 | pub markets: Vec
, 64 | pub assets: HashMap, 65 | pub price_feeds: HashMap, 66 | pub price_decimals: U256, 67 | } 68 | 69 | #[derive(Default, Debug)] 70 | pub struct LiquidationData { 71 | pub liquidations: HashMap, 72 | pub action: LiquidationAction, 73 | pub state: ProtocolState, 74 | } 75 | 76 | pub struct Liquidation { 77 | pub client: Arc>, 78 | token_pairs: Arc, 79 | tokens: Arc>, 80 | liquidator: Liquidator>, 81 | previewer: Previewer>, 82 | auditor: Auditor>, 83 | market_weth_address: Address, 84 | backup: u32, 85 | liquidate_unprofitable: bool, 86 | repay_offset: U256, 87 | network: Arc, 88 | } 89 | 90 | impl< 91 | M: 'static + Middleware + Clone, 92 | W: 'static + Middleware + Clone, 93 | S: 'static + Signer + Clone, 94 | > Liquidation 95 | { 96 | #[allow(clippy::too_many_arguments)] 97 | pub fn new( 98 | client: Arc>, 99 | client_relay: Arc>, 100 | token_pairs: &str, 101 | previewer: Previewer>, 102 | auditor: Auditor>, 103 | market_weth_address: Address, 104 | config: &Config, 105 | network: Arc, 106 | ) -> Self { 107 | let (token_pairs, tokens) = parse_token_pairs(token_pairs); 108 | let token_pairs = Arc::new(token_pairs); 109 | let tokens = Arc::new(tokens); 110 | let liquidator = 111 | Self::get_contracts(Arc::clone(&client_relay), config.chain_id_name.clone()); 112 | Self { 113 | client, 114 | token_pairs, 115 | tokens, 116 | liquidator, 117 | previewer, 118 | auditor, 119 | market_weth_address, 120 | backup: config.backup, 121 | liquidate_unprofitable: config.liquidate_unprofitable, 122 | repay_offset: config.repay_offset, 123 | network, 124 | } 125 | } 126 | 127 | fn get_contracts( 128 | client_relayer: Arc>, 129 | chain_id_name: String, 130 | ) -> Liquidator> { 131 | let (liquidator_address, _, _) = Protocol::::parse_abi(&format!( 132 | "deployments/{}/Liquidator.json", 133 | chain_id_name 134 | )); 135 | Liquidator::new(liquidator_address, Arc::clone(&client_relayer)) 136 | } 137 | 138 | pub fn get_tokens(&self) -> Arc> { 139 | Arc::clone(&self.tokens) 140 | } 141 | 142 | pub fn get_token_pairs(&self) -> Arc { 143 | Arc::clone(&self.token_pairs) 144 | } 145 | 146 | pub async fn run( 147 | this: Arc>, 148 | mut receiver: Receiver, 149 | ) -> Result<()> { 150 | let mut liquidations = HashMap::new(); 151 | let mut liquidations_iter = None; 152 | let mut state = ProtocolState::default(); 153 | let backup = this.lock().await.backup; 154 | let d = Duration::from_millis(1); 155 | loop { 156 | match time::timeout(d, receiver.recv()).await { 157 | Ok(Some(data)) => { 158 | match data.action { 159 | LiquidationAction::Update => { 160 | info!("Updating liquidation data"); 161 | liquidations = data 162 | .liquidations 163 | .into_iter() 164 | .map(|(account_address, (account, repay))| { 165 | let age = if backup > 0 { 166 | liquidations 167 | .get(&account_address) 168 | .map(|(_, _, age)| *age) 169 | .unwrap_or(0) 170 | + 1 171 | } else { 172 | 0 173 | }; 174 | (account_address, (account, repay, age)) 175 | }) 176 | .collect(); 177 | } 178 | LiquidationAction::Insert => { 179 | let mut new_liquidations = data.liquidations; 180 | for (k, v) in new_liquidations.drain() { 181 | let liquidation = liquidations.entry(k).or_insert((v.0, v.1, 0)); 182 | if backup > 0 { 183 | liquidation.2 += 1; 184 | } 185 | } 186 | } 187 | } 188 | liquidations_iter = Some(liquidations.iter()); 189 | state.gas_price = data.state.gas_price; 190 | state.markets = data.state.markets; 191 | state.price_feeds = data.state.price_feeds; 192 | state.price_decimals = data.state.price_decimals; 193 | state.assets = data.state.assets; 194 | } 195 | Ok(None) => {} 196 | Err(_) => { 197 | if let Some(liquidation) = &mut liquidations_iter { 198 | info!("Check for next to liquidate"); 199 | if let Some((_, (account, repay, age))) = liquidation.next() { 200 | info!("Found"); 201 | if backup == 0 || *age > backup { 202 | if backup > 0 { 203 | info!("backup liquidation - {}", age); 204 | } 205 | let _ = this.lock().await.liquidate(account, repay, &state).await; 206 | } else { 207 | info!("backup - not old enough: {}", age); 208 | } 209 | } else { 210 | info!("Not found"); 211 | liquidations_iter = None; 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | 219 | async fn liquidate( 220 | &self, 221 | account: &Account, 222 | repay: &Repay, 223 | state: &ProtocolState, 224 | ) -> Result<()> { 225 | info!("Liquidating account {:?}", account); 226 | if let Some(address) = &repay.market_to_repay { 227 | let response = self.is_profitable_async(account.address, state).await; 228 | 229 | let ((profitable, profit, cost), repay_settings, gas_used, will_revert) = match response 230 | { 231 | Some(response) => response, 232 | None => return Ok(()), 233 | }; 234 | gen_liq_breadcrumb(account, repay, &repay_settings); 235 | if will_revert { 236 | error!("Liquidation would revert - not sent"); 237 | return Ok(()); 238 | } 239 | 240 | println!("profitable: {}", profitable); 241 | println!( 242 | "self.liquidate_unprofitable: {}", 243 | self.liquidate_unprofitable 244 | ); 245 | if !profitable && !self.liquidate_unprofitable { 246 | gen_liq_breadcrumb(account, repay, &repay_settings); 247 | warn!( 248 | "liquidation not profitable for {:#?} (profit: {:#?} cost: {:#?})", 249 | account.address, profit, cost 250 | ); 251 | return Ok(()); 252 | } 253 | 254 | info!("Liquidating on market {:#?}", address); 255 | info!("seizing {:#?}", repay.market_to_seize); 256 | 257 | // liquidate using liquidator contract 258 | info!("repay : {:#?}", *address); 259 | info!( 260 | "seize : {:#?}", 261 | repay.market_to_seize.unwrap_or(Address::zero()) 262 | ); 263 | info!("borrower : {:#?}", account.address); 264 | info!("max_repay : {:#?}", repay_settings.max_repay); 265 | info!("pool_pair : {:#?}", repay_settings.pool_pair); 266 | info!("pair_fee : {:#?}", repay_settings.pair_fee); 267 | info!("fee : {:#?}", repay_settings.fee); 268 | 269 | let func = self 270 | .liquidator 271 | .liquidate( 272 | *address, 273 | repay.market_to_seize.unwrap_or(Address::zero()), 274 | account.address, 275 | repay_settings.max_repay, 276 | repay_settings.pool_pair, 277 | repay_settings.pair_fee, 278 | repay_settings.fee, 279 | ) 280 | // Increase gas cost by 20% 281 | .gas( 282 | gas_used 283 | .unwrap_or(self.network.default_gas_used()) 284 | .mul_div_down(U256::from(150), U256::from(100)), 285 | ); 286 | info!("func: {:#?}", func); 287 | let tx = func.send().await; 288 | info!("tx: {:#?}", &tx); 289 | let tx = tx?; 290 | info!("waiting receipt"); 291 | let receipt = tx.confirmations(1).await?; 292 | info!("Liquidation tx {:?}", receipt); 293 | let (adjusted_collateral, adjusted_debt): (U256, U256) = self 294 | .auditor 295 | .account_liquidity(account.address, Address::zero(), U256::zero()) 296 | .call() 297 | .await?; 298 | let hf = adjusted_collateral.div_wad_down(adjusted_debt); 299 | if hf < math::RECOVERY_THRESHOLD { 300 | info!("hf : {:#?}", hf); 301 | info!("collateral: {:#?}", adjusted_collateral); 302 | info!("debt : {:#?}", adjusted_debt); 303 | error!("liquidation failed"); 304 | return Ok(()); 305 | } 306 | } 307 | info!("done liquidating"); 308 | Ok(()) 309 | } 310 | 311 | pub async fn is_profitable_async( 312 | &self, 313 | account: Address, 314 | state: &ProtocolState, 315 | ) -> Option<((bool, U256, U256), RepaySettings, Option, bool)> { 316 | let mut multicall = 317 | Multicall::>::new(Arc::clone(&self.client), None) 318 | .await 319 | .unwrap(); 320 | multicall.add_call(self.previewer.exactly(account), false); 321 | multicall.add_call( 322 | self.auditor 323 | .account_liquidity(account, Address::zero(), U256::zero()), 324 | false, 325 | ); 326 | multicall.add_call(self.auditor.liquidation_incentive(), false); 327 | 328 | let mut price_multicall = 329 | Multicall::>::new(Arc::clone(&self.client), None) 330 | .await 331 | .unwrap(); 332 | state.markets.iter().for_each(|market| { 333 | if state.price_feeds[market] != Address::zero() 334 | && state.price_feeds[market] != Address::from_str(protocol::BASE_FEED).unwrap() 335 | { 336 | price_multicall 337 | .add_call(self.auditor.asset_price(state.price_feeds[market]), false); 338 | } 339 | }); 340 | let multicall = multicall.version(MulticallVersion::Multicall); 341 | price_multicall = price_multicall.version(MulticallVersion::Multicall); 342 | let response = tokio::try_join!(multicall.call(), price_multicall.call_raw()); 343 | 344 | let (data, prices) = if let Ok(response) = response { 345 | response 346 | } else { 347 | if let Err(err) = response { 348 | info!("error: {:?}", err); 349 | } 350 | info!("error getting multicall data"); 351 | return None; 352 | }; 353 | 354 | let (market_account, (adjusted_collateral, adjusted_debt), liquidation_incentive): ( 355 | Vec, 356 | (U256, U256), 357 | LiquidationIncentive, 358 | ) = data; 359 | 360 | let mut i = 0; 361 | let prices: HashMap = state 362 | .markets 363 | .iter() 364 | .map(|market| { 365 | if state.price_feeds[market] != Address::zero() 366 | && state.price_feeds[market] != Address::from_str(protocol::BASE_FEED).unwrap() 367 | { 368 | let price = (*market, prices[i].clone().unwrap().into_uint().unwrap()); 369 | i += 1; 370 | price 371 | } else { 372 | (*market, U256::exp10(state.price_decimals.as_usize())) 373 | } 374 | }) 375 | .collect(); 376 | 377 | if adjusted_debt.is_zero() { 378 | info!("no debt"); 379 | return None; 380 | } 381 | let hf = adjusted_collateral.div_wad_down(adjusted_debt); 382 | if hf > math::RECOVERY_THRESHOLD { 383 | info!("in recovery"); 384 | return None; 385 | } 386 | let timestamp = SystemTime::now() 387 | .duration_since(SystemTime::UNIX_EPOCH) 388 | .unwrap() 389 | .as_secs(); 390 | let repay = Self::pick_markets(&market_account, &prices, timestamp.into(), &state.assets); 391 | let repay_settings = Self::get_liquidation_settings( 392 | &repay, 393 | &liquidation_incentive, 394 | self.repay_offset, 395 | &self.token_pairs, 396 | &self.tokens, 397 | ); 398 | 399 | // check if transaction will revert 400 | let gas_used = if let Some(address) = &repay.market_to_repay { 401 | let func = self.liquidator.liquidate( 402 | *address, 403 | repay.market_to_seize.unwrap_or(Address::zero()), 404 | account, 405 | repay_settings.max_repay, 406 | repay_settings.pool_pair, 407 | repay_settings.pair_fee, 408 | repay_settings.fee, 409 | ); 410 | 411 | match func.estimate_gas().await { 412 | Ok(gas) => Some(gas), 413 | Err(err) => { 414 | let mut data = BTreeMap::new(); 415 | data.insert("message".to_string(), Value::String(err.to_string())); 416 | sentry::add_breadcrumb(Breadcrumb { 417 | ty: "error".to_string(), 418 | category: Some("estimating gas".to_string()), 419 | level: Level::Error, 420 | data, 421 | ..Default::default() 422 | }); 423 | return Some(( 424 | (false, U256::zero(), U256::zero()), 425 | repay_settings, 426 | None, 427 | true, 428 | )); 429 | } 430 | } 431 | } else { 432 | return None; 433 | }; 434 | 435 | Some(( 436 | Self::is_profitable( 437 | &self.network, 438 | &repay, 439 | &repay_settings, 440 | &liquidation_incentive, 441 | NetworkStatus { 442 | gas_price: state.gas_price, 443 | gas_used, 444 | eth_price: prices[&self.market_weth_address], 445 | }, 446 | ), 447 | repay_settings, 448 | gas_used, 449 | false, 450 | )) 451 | } 452 | 453 | pub fn pick_markets( 454 | market_account: &Vec, 455 | prices: &HashMap, 456 | timestamp: U256, 457 | assets: &HashMap, 458 | ) -> Repay { 459 | let mut repay = Repay::default(); 460 | for market in market_account { 461 | if market.is_collateral { 462 | let collateral_value = market.floating_deposit_assets.mul_div_down( 463 | prices[&market.market], 464 | U256::exp10(market.decimals as usize), 465 | ); 466 | let adjusted_collateral = collateral_value.mul_wad_down(market.adjust_factor); 467 | repay.total_value_collateral += collateral_value; 468 | repay.total_adjusted_collateral += adjusted_collateral; 469 | if adjusted_collateral >= repay.market_to_seize_value { 470 | repay.market_to_seize_value = adjusted_collateral; 471 | repay.market_to_seize = Some(market.market); 472 | repay.collateral_asset_address = assets[&market.market]; 473 | } 474 | }; 475 | let mut market_debt_assets = U256::zero(); 476 | for fixed_position in &market.fixed_borrow_positions { 477 | let borrowed = fixed_position.position.principal + fixed_position.position.fee; 478 | market_debt_assets += borrowed; 479 | if fixed_position.maturity < timestamp { 480 | market_debt_assets += borrowed 481 | .mul_wad_down((timestamp - fixed_position.maturity) * market.penalty_rate) 482 | } 483 | } 484 | market_debt_assets += market.floating_borrow_assets; 485 | let market_debt_value = market_debt_assets.mul_div_up( 486 | prices[&market.market], 487 | U256::exp10(market.decimals as usize), 488 | ); 489 | let adjusted_debt = market_debt_value.div_wad_up(market.adjust_factor); 490 | repay.total_value_debt += market_debt_value; 491 | repay.total_adjusted_debt += adjusted_debt; 492 | if adjusted_debt >= repay.market_to_liquidate_debt { 493 | repay.market_to_liquidate_debt = adjusted_debt; 494 | repay.market_to_repay = Some(market.market); 495 | repay.price = prices[&market.market]; 496 | repay.decimals = market.decimals; 497 | repay.repay_asset_address = assets[&market.market]; 498 | } 499 | } 500 | repay 501 | } 502 | 503 | #[allow(clippy::too_many_arguments)] 504 | pub fn is_profitable( 505 | network: &Network, 506 | repay: &Repay, 507 | repay_settings: &RepaySettings, 508 | liquidation_incentive: &LiquidationIncentive, 509 | network_status: NetworkStatus, 510 | ) -> (bool, U256, U256) { 511 | let profit = Self::max_profit(repay, repay_settings.max_repay, liquidation_incentive); 512 | let cost = Self::max_cost( 513 | network, 514 | repay, 515 | repay_settings.max_repay, 516 | liquidation_incentive, 517 | U256::from(repay_settings.pair_fee), 518 | U256::from(repay_settings.fee), 519 | network_status, 520 | ); 521 | ( 522 | profit > cost && profit - cost > math::WAD / U256::exp10(16), 523 | profit, 524 | cost, 525 | ) 526 | } 527 | 528 | fn get_flash_pair( 529 | repay: &Repay, 530 | token_pairs: &HashMap<(Address, Address), BinaryHeap>>, 531 | tokens: &HashSet
, 532 | ) -> (Address, u32, u32) { 533 | let collateral = repay.collateral_asset_address; 534 | let repay = repay.repay_asset_address; 535 | 536 | let mut lowest_fee = u32::MAX; 537 | let mut pair_contract = Address::zero(); 538 | 539 | if collateral != repay { 540 | if let Some(pair) = token_pairs.get(&ordered_addresses(collateral, repay)) { 541 | return (Address::zero(), pair.peek().unwrap().0, 0); 542 | } else { 543 | let (route, _) = token_pairs.iter().fold( 544 | (Address::zero(), u32::MAX), 545 | |(address, lowest_fee), ((token0, token1), pair_fee)| { 546 | let route = if *token0 == repay { 547 | token1 548 | } else if *token1 == repay { 549 | token0 550 | } else { 551 | return (address, lowest_fee); 552 | }; 553 | if let Some(fee) = token_pairs.get(&ordered_addresses(collateral, *route)) { 554 | let total_fee = pair_fee.peek().unwrap().0 + fee.peek().unwrap().0; 555 | if total_fee < lowest_fee { 556 | return (*route, total_fee); 557 | } 558 | } 559 | (address, lowest_fee) 560 | }, 561 | ); 562 | return match ( 563 | token_pairs.get(&ordered_addresses(route, repay)), 564 | token_pairs.get(&ordered_addresses(collateral, route)), 565 | ) { 566 | (Some(pair_fee), Some(fee)) => { 567 | (route, pair_fee.peek().unwrap().0, fee.peek().unwrap().0) 568 | } 569 | 570 | _ => (Address::zero(), 0, 0), 571 | }; 572 | } 573 | } 574 | 575 | for token in tokens { 576 | if *token != collateral { 577 | if let Some(pair) = token_pairs.get(&ordered_addresses(*token, collateral)) { 578 | if let Some(rate) = pair.peek() { 579 | if rate.0 < lowest_fee { 580 | lowest_fee = rate.0; 581 | pair_contract = *token; 582 | } 583 | } 584 | } 585 | } 586 | } 587 | (pair_contract, lowest_fee, 0) 588 | } 589 | 590 | fn max_repay_assets( 591 | repay: &Repay, 592 | liquidation_incentive: &LiquidationIncentive, 593 | max_liquidator_assets: U256, 594 | ) -> U256 { 595 | let close_factor = Self::calculate_close_factor(repay, liquidation_incentive); 596 | U256::min( 597 | U256::min( 598 | repay 599 | .total_value_debt 600 | .mul_wad_up(U256::min(math::WAD, close_factor)), 601 | repay.market_to_seize_value.div_wad_up( 602 | math::WAD + liquidation_incentive.liquidator + liquidation_incentive.lenders, 603 | ), 604 | ) 605 | .mul_div_up(U256::exp10(repay.decimals as usize), repay.price), 606 | if max_liquidator_assets 607 | < U256::from_str("115792089237316195423570985008687907853269984665640564039457") //// U256::MAX / WAD 608 | .unwrap() 609 | { 610 | max_liquidator_assets.div_wad_down(math::WAD + liquidation_incentive.lenders) 611 | } else { 612 | max_liquidator_assets 613 | }, 614 | ) 615 | .min(repay.market_to_liquidate_debt) 616 | } 617 | 618 | fn max_profit( 619 | repay: &Repay, 620 | max_repay: U256, 621 | liquidation_incentive: &LiquidationIncentive, 622 | ) -> U256 { 623 | max_repay 624 | .mul_div_up(repay.price, U256::exp10(repay.decimals as usize)) 625 | .mul_wad_down(U256::from( 626 | liquidation_incentive.liquidator + liquidation_incentive.lenders, 627 | )) 628 | } 629 | 630 | #[allow(clippy::too_many_arguments)] 631 | fn max_cost( 632 | network: &Network, 633 | repay: &Repay, 634 | max_repay: U256, 635 | liquidation_incentive: &LiquidationIncentive, 636 | swap_pair_fee: U256, 637 | swap_fee: U256, 638 | network_status: NetworkStatus, 639 | ) -> U256 { 640 | let max_repay = max_repay.mul_div_down(repay.price, U256::exp10(repay.decimals as usize)); 641 | max_repay.mul_wad_down(U256::from(liquidation_incentive.lenders)) 642 | + max_repay.mul_wad_down(swap_fee * U256::exp10(12)) 643 | + max_repay.mul_wad_down(swap_pair_fee * U256::exp10(12)) 644 | + network.tx_cost(network_status) 645 | } 646 | 647 | pub fn calculate_close_factor( 648 | repay: &Repay, 649 | liquidation_incentive: &LiquidationIncentive, 650 | ) -> U256 { 651 | let target_health = U256::exp10(16usize) * 125u32; 652 | let adjust_factor = repay 653 | .total_adjusted_collateral 654 | .mul_wad_down(repay.total_value_debt) 655 | .div_wad_up( 656 | repay 657 | .total_adjusted_debt 658 | .mul_wad_up(repay.total_value_collateral), 659 | ); 660 | (target_health 661 | - repay 662 | .total_adjusted_collateral 663 | .div_wad_up(repay.total_adjusted_debt)) 664 | .div_wad_up( 665 | target_health 666 | - adjust_factor.mul_wad_down( 667 | math::WAD 668 | + liquidation_incentive.liquidator 669 | + liquidation_incentive.lenders 670 | + U256::from(liquidation_incentive.liquidator) 671 | .mul_wad_down(liquidation_incentive.lenders.into()), 672 | ), 673 | ) 674 | } 675 | 676 | pub fn set_liquidator( 677 | &mut self, 678 | client_relay: Arc>, 679 | chain_id_name: String, 680 | ) { 681 | self.liquidator = Self::get_contracts(Arc::clone(&client_relay), chain_id_name); 682 | } 683 | 684 | pub fn get_liquidation_settings( 685 | repay: &Repay, 686 | liquidation_incentive: &LiquidationIncentive, 687 | repay_offset: U256, 688 | token_pairs: &HashMap<(Address, Address), BinaryHeap>>, 689 | tokens: &HashSet
, 690 | ) -> RepaySettings { 691 | let max_repay = Self::max_repay_assets(repay, liquidation_incentive, U256::MAX) 692 | .mul_wad_down(math::WAD + U256::exp10(14)) 693 | + repay_offset.mul_div_up(U256::exp10(repay.decimals as usize), repay.price); 694 | let (pool_pair, pair_fee, fee): (Address, u32, u32) = 695 | Self::get_flash_pair(repay, token_pairs, tokens); 696 | RepaySettings { 697 | max_repay, 698 | pool_pair, 699 | pair_fee, 700 | fee, 701 | } 702 | } 703 | } 704 | 705 | pub fn gen_liq_breadcrumb(account: &Account, repay: &Repay, repay_settings: &RepaySettings) { 706 | let mut data = BTreeMap::new(); 707 | data.insert( 708 | "account".to_string(), 709 | Value::String(account.address.to_string()), 710 | ); 711 | data.insert( 712 | "market to liquidate".to_string(), 713 | Value::String( 714 | repay 715 | .market_to_repay 716 | .map(|v| v.to_string()) 717 | .unwrap_or("market empty".to_string()), 718 | ), 719 | ); 720 | data.insert( 721 | "market to seize".to_string(), 722 | Value::String( 723 | repay 724 | .market_to_seize 725 | .map(|v| v.to_string()) 726 | .unwrap_or("market empty".to_string()), 727 | ), 728 | ); 729 | data.insert( 730 | "value to seize".to_string(), 731 | Value::String(repay.market_to_seize_value.to_string()), 732 | ); 733 | data.insert( 734 | "collateral value".to_string(), 735 | Value::String(repay.total_value_collateral.to_string()), 736 | ); 737 | data.insert( 738 | "debt value".to_string(), 739 | Value::String(repay.total_value_debt.to_string()), 740 | ); 741 | data.insert( 742 | "repay value".to_string(), 743 | Value::String(repay_settings.max_repay.to_string()), 744 | ); 745 | data.insert( 746 | "pool pair".to_string(), 747 | Value::String(repay_settings.pool_pair.to_string()), 748 | ); 749 | data.insert( 750 | "pool fee".to_string(), 751 | Value::String(repay_settings.fee.to_string()), 752 | ); 753 | data.insert( 754 | "pool pair fee".to_string(), 755 | Value::String(repay_settings.pair_fee.to_string()), 756 | ); 757 | sentry::add_breadcrumb(Breadcrumb { 758 | ty: "error".to_string(), 759 | category: Some("Reverting Liquidation".to_string()), 760 | level: Level::Error, 761 | data, 762 | ..Default::default() 763 | }); 764 | } 765 | 766 | fn ordered_addresses(token0: Address, token1: Address) -> (Address, Address) { 767 | if token0 < token1 { 768 | (token0, token1) 769 | } else { 770 | (token1, token0) 771 | } 772 | } 773 | 774 | #[derive(Deserialize, Debug)] 775 | pub struct TokenPair { 776 | pub token0: String, 777 | pub token1: String, 778 | pub fee: u32, 779 | } 780 | 781 | fn parse_token_pairs(token_pairs: &str) -> (TokenPairFeeMap, HashSet
) { 782 | let mut tokens = HashSet::new(); 783 | let json_pairs: Vec<(String, String, u32)> = serde_json::from_str(token_pairs).unwrap(); 784 | let mut pairs = HashMap::new(); 785 | for (token0, token1, fee) in json_pairs { 786 | let token0 = Address::from_str(&token0).unwrap(); 787 | let token1 = Address::from_str(&token1).unwrap(); 788 | tokens.insert(token0); 789 | tokens.insert(token1); 790 | pairs 791 | .entry(ordered_addresses(token0, token1)) 792 | .or_insert_with(BinaryHeap::new) 793 | .push(Reverse(fee)); 794 | } 795 | (pairs, tokens) 796 | } 797 | 798 | #[cfg(test)] 799 | mod services_test { 800 | 801 | // Note this useful idiom: importing names from outer (for mod tests) scope. 802 | use super::*; 803 | 804 | #[test] 805 | fn test_parse_token_pairs() { 806 | let tokens = r#"[[ 807 | "0x0000000000000000000000000000000000000000", 808 | "0x0000000000000000000000000000000000000001", 809 | 3000 810 | ], 811 | [ 812 | "0x0000000000000000000000000000000000000000", 813 | "0x0000000000000000000000000000000000000001", 814 | 1000 815 | ], 816 | [ 817 | "0x0000000000000000000000000000000000000000", 818 | "0x0000000000000000000000000000000000000001", 819 | 2000 820 | ]]"#; 821 | let (pairs, _) = parse_token_pairs(tokens); 822 | assert_eq!( 823 | pairs 824 | .get( 825 | &(ordered_addresses( 826 | Address::from_str("0x0000000000000000000000000000000000000001").unwrap(), 827 | Address::from_str("0x0000000000000000000000000000000000000000").unwrap() 828 | )) 829 | ) 830 | .unwrap() 831 | .peek() 832 | .unwrap() 833 | .0, 834 | 1000 835 | ); 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::panic; 3 | use std::process; 4 | use std::sync::Arc; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | use ethers::prelude::k256::ecdsa::SigningKey; 9 | use ethers::prelude::{Provider, Signer, SignerMiddleware, Wallet, Ws}; 10 | use ethers::providers::Http; 11 | use eyre::Result; 12 | use url::Url; 13 | 14 | mod account; 15 | mod config; 16 | mod exactly_events; 17 | mod market; 18 | mod network; 19 | mod protocol; 20 | 21 | mod fixed_point_math; 22 | mod liquidation; 23 | 24 | mod generate_abi; 25 | 26 | pub use account::*; 27 | pub use exactly_events::*; 28 | use log::error; 29 | use log::info; 30 | pub use market::Market; 31 | pub use protocol::Protocol; 32 | pub use protocol::ProtocolError; 33 | use sentry::integrations::log::SentryLogger; 34 | use sentry::Breadcrumb; 35 | use sentry::Level; 36 | use serde_json::Value; 37 | use tokio::sync::Mutex; 38 | 39 | use crate::config::Config; 40 | 41 | type ExactlyProtocol = Protocol, Provider, Wallet>; 42 | 43 | const RETRIES: usize = 10; 44 | 45 | enum ConnectFrom { 46 | Ws(String), 47 | Https(String), 48 | } 49 | 50 | enum ConnectResult { 51 | Ws(Provider), 52 | Https(Provider), 53 | } 54 | 55 | async fn connect_ws(provider: &str) -> Result { 56 | let provider = Provider::::connect(provider).await?; 57 | Ok(ConnectResult::Ws(provider)) 58 | } 59 | 60 | async fn connect_https(provider_address: &str) -> Result { 61 | let url = Url::parse(provider_address)?; 62 | let client = reqwest::Client::builder() 63 | .connect_timeout(Duration::from_secs(60)) 64 | .timeout(Duration::from_secs(60)) 65 | .build()?; 66 | let http_provider = Http::new_with_client(url, client); 67 | let provider = Provider::new(http_provider); 68 | Ok(ConnectResult::Https(provider)) 69 | } 70 | 71 | async fn retry_connect(provider: ConnectFrom) -> ConnectResult { 72 | let mut counter = 0; 73 | loop { 74 | let (provider_url, provider_result) = match &provider { 75 | ConnectFrom::Ws(provider) => (provider.clone(), connect_ws(provider).await), 76 | ConnectFrom::Https(provider) => (provider.clone(), connect_https(provider).await), 77 | }; 78 | match provider_result { 79 | Ok(provider) => { 80 | break provider; 81 | } 82 | Err(ref e) => { 83 | if counter == RETRIES { 84 | let mut data = BTreeMap::new(); 85 | data.insert( 86 | "error message".to_string(), 87 | Value::String(format!("{:?}", e)), 88 | ); 89 | data.insert("connecting to".to_string(), Value::String(provider_url)); 90 | sentry::add_breadcrumb(Breadcrumb { 91 | ty: "error".to_string(), 92 | category: Some("Trying to connect".to_string()), 93 | level: Level::Error, 94 | data, 95 | ..Default::default() 96 | }); 97 | panic!( 98 | "Failed to connect to provider after {} retries\nerror:{:?}", 99 | RETRIES, e 100 | ); 101 | } 102 | counter += 1; 103 | thread::sleep(Duration::from_secs(1)); 104 | } 105 | } 106 | thread::sleep(Duration::from_secs(1)); 107 | } 108 | } 109 | 110 | async fn create_client( 111 | config: &Config, 112 | ) -> ( 113 | Arc, Wallet>>, 114 | Arc, Wallet>>, 115 | ) { 116 | let ConnectResult::Ws(provider_ws) = 117 | retry_connect(ConnectFrom::Ws(config.rpc_provider.clone())).await 118 | else { 119 | panic!("Failed to connect to provider after {} retries", RETRIES); 120 | }; 121 | let ConnectResult::Https(provider_https) = 122 | retry_connect(ConnectFrom::Https(config.rpc_provider_relayer.clone())).await 123 | else { 124 | panic!("Failed to connect to provider after {} retries", RETRIES); 125 | }; 126 | let wallet = config.wallet.clone().with_chain_id(config.chain_id); 127 | ( 128 | Arc::new(SignerMiddleware::new(provider_ws, wallet.clone())), 129 | Arc::new(SignerMiddleware::new(provider_https, wallet)), 130 | ) 131 | } 132 | 133 | #[tokio::main] 134 | async fn main() -> Result<()> { 135 | let mut log_builder = pretty_env_logger::formatted_builder(); 136 | log_builder.parse_filters("warn,info,debug"); 137 | let logger = SentryLogger::with_dest(log_builder.build()); 138 | 139 | log::set_boxed_logger(Box::new(logger)).unwrap(); 140 | if cfg!(debug_assertions) { 141 | log::set_max_level(log::LevelFilter::Debug); 142 | } else { 143 | log::set_max_level(log::LevelFilter::Info); 144 | } 145 | 146 | let config = Config::default(); 147 | 148 | let _guard = config.sentry_dsn.clone().map(|sentry_dsn| { 149 | sentry::init(( 150 | sentry_dsn, 151 | sentry::ClientOptions { 152 | release: sentry::release_name!(), 153 | debug: true, 154 | attach_stacktrace: true, 155 | default_integrations: true, 156 | max_breadcrumbs: 1000, 157 | traces_sample_rate: 1.0, 158 | ..Default::default() 159 | }, 160 | )) 161 | }); 162 | 163 | panic::set_hook(Box::new(|panic_info| { 164 | let mut data = BTreeMap::new(); 165 | data.insert( 166 | "message".to_string(), 167 | Value::String(format!("{:?}", panic_info)), 168 | ); 169 | if let Some(location) = panic_info.location() { 170 | data.insert("message".to_string(), Value::String(location.to_string())); 171 | } 172 | 173 | sentry::add_breadcrumb(Breadcrumb { 174 | ty: "error".to_string(), 175 | category: Some("panic".to_string()), 176 | level: Level::Error, 177 | data, 178 | ..Default::default() 179 | }); 180 | 181 | if let Some(client) = sentry::Hub::current().client() { 182 | client.close(Some(Duration::from_secs(2))); 183 | } 184 | process::abort(); 185 | })); 186 | 187 | info!("Liquidation bot v{} starting", env!("CARGO_PKG_VERSION")); 188 | dbg!(&config); 189 | 190 | let mut credit_service: Option>> = None; 191 | let mut update_client = false; 192 | let mut last_client = None; 193 | loop { 194 | if let Some((client, client_relayer)) = &last_client { 195 | if let Some(service) = &mut credit_service { 196 | if update_client { 197 | info!("Updating client"); 198 | info!("service reference count: {}", Arc::strong_count(service)); 199 | if let Ok(service) = &mut service.try_lock() { 200 | service 201 | .update_client(Arc::clone(client), Arc::clone(client_relayer), &config) 202 | .await; 203 | } else { 204 | panic!("service is locked"); 205 | } 206 | update_client = false; 207 | } 208 | } else { 209 | info!("creating service"); 210 | credit_service = Some(Arc::new(Mutex::new( 211 | Protocol::new(Arc::clone(client), Arc::clone(client_relayer), &config).await?, 212 | ))); 213 | } 214 | if let Some(service) = &credit_service { 215 | info!("launching service"); 216 | match Protocol::launch(Arc::clone(service)).await { 217 | Ok(()) => { 218 | break; 219 | } 220 | Err(e) => { 221 | let mut data = BTreeMap::new(); 222 | data.insert( 223 | "Protocol error".to_string(), 224 | Value::String(format!("{:?}", e)), 225 | ); 226 | match e { 227 | ProtocolError::SignerMiddlewareError(e) => { 228 | data.insert( 229 | "Connection error".to_string(), 230 | Value::String(format!("{:?}", e)), 231 | ); 232 | sentry::add_breadcrumb(Breadcrumb { 233 | ty: "error".to_string(), 234 | category: Some("Connection".to_string()), 235 | level: Level::Error, 236 | data, 237 | ..Default::default() 238 | }); 239 | 240 | update_client = true; 241 | last_client = None; 242 | } 243 | _ => { 244 | sentry::add_breadcrumb(Breadcrumb { 245 | ty: "error".to_string(), 246 | category: Some("General error".to_string()), 247 | level: Level::Error, 248 | data, 249 | ..Default::default() 250 | }); 251 | 252 | error!("ERROR"); 253 | break; 254 | } 255 | } 256 | } 257 | } 258 | } 259 | } else { 260 | last_client = Some(create_client(&config).await); 261 | } 262 | } 263 | Ok(()) 264 | } 265 | -------------------------------------------------------------------------------- /src/market.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use ethers::abi::Address; 5 | use ethers::prelude::{abigen, Middleware, Signer, SignerMiddleware, U256}; 6 | 7 | use ethers::types::I256; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use super::fixed_point_math::{FixedPointMath, FixedPointMathGen}; 11 | 12 | const INTERVAL: u32 = 4 * 7 * 86_400; 13 | 14 | abigen!( 15 | ERC20, 16 | "node_modules/@exactly/protocol/deployments/goerli/DAI.json", 17 | event_derives(serde::Deserialize, serde::Serialize) 18 | ); 19 | 20 | #[derive(Clone, Copy, Default, Debug, Serialize, Deserialize)] 21 | pub struct PriceRate { 22 | pub address: Address, 23 | pub conversion_selector: [u8; 4], 24 | pub base_unit: U256, 25 | pub main_price: U256, 26 | pub rate: U256, 27 | pub event_emitter: Option
, 28 | } 29 | 30 | #[derive(Clone, Copy, Default, Debug, Serialize, Deserialize)] 31 | pub struct PriceDouble { 32 | pub price_feed_one: Address, 33 | pub price_feed_two: Address, 34 | pub base_unit: U256, 35 | pub decimals: U256, 36 | pub price_one: U256, 37 | pub price_two: U256, 38 | } 39 | 40 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 41 | pub enum PriceFeedType { 42 | Single(PriceRate), 43 | Double(PriceDouble), 44 | } 45 | 46 | #[derive(Clone, Default, Debug, Serialize, Deserialize)] 47 | pub struct PriceFeedController { 48 | pub address: Address, 49 | pub main_price_feed: Option>, 50 | pub event_emitters: Vec
, 51 | pub wrapper: Option, 52 | } 53 | 54 | impl PriceFeedController { 55 | pub fn main_price_feed(address: Address, event_emitters: Option>) -> Self { 56 | Self { 57 | address, 58 | main_price_feed: None, 59 | event_emitters: event_emitters.unwrap_or_default(), 60 | wrapper: None, 61 | } 62 | } 63 | } 64 | 65 | #[derive(Eq, PartialEq, Debug, Default, Serialize, Deserialize)] 66 | pub struct FixedPool { 67 | pub borrowed: U256, 68 | pub supplied: U256, 69 | pub unassigned_earnings: U256, 70 | pub last_accrual: U256, 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Default, Debug)] 74 | pub struct Market { 75 | pub contract: Address, 76 | pub interest_rate_model: Address, 77 | pub price: U256, 78 | pub penalty_rate: U256, 79 | pub adjust_factor: U256, 80 | pub decimals: u8, 81 | pub floating_assets: U256, 82 | pub floating_deposit_shares: U256, 83 | pub floating_debt: U256, 84 | pub floating_borrow_shares: U256, 85 | pub floating_utilization: U256, 86 | pub last_floating_debt_update: U256, 87 | pub max_future_pools: u8, 88 | pub fixed_pools: HashMap, 89 | pub smart_pool_fee_rate: U256, 90 | pub earnings_accumulator: U256, 91 | pub last_accumulator_accrual: U256, 92 | pub earnings_accumulator_smooth_factor: U256, 93 | pub price_feed: Option, 94 | pub listed: bool, 95 | pub floating_full_utilization: u128, 96 | pub floating_a: U256, 97 | pub floating_b: i128, 98 | pub floating_max_utilization: U256, 99 | pub treasury_fee_rate: U256, 100 | pub asset: Address, 101 | pub base_market: bool, 102 | pub symbol: String, 103 | } 104 | 105 | impl Eq for Market {} 106 | 107 | impl PartialEq for Market { 108 | fn eq(&self, other: &Self) -> bool { 109 | self.contract == other.contract 110 | } 111 | } 112 | 113 | impl Market { 114 | pub fn new(address: Address) -> Self { 115 | Self { 116 | contract: address, 117 | interest_rate_model: Default::default(), 118 | price: Default::default(), 119 | penalty_rate: Default::default(), 120 | adjust_factor: Default::default(), 121 | decimals: Default::default(), 122 | floating_assets: Default::default(), 123 | floating_deposit_shares: Default::default(), 124 | floating_debt: Default::default(), 125 | floating_borrow_shares: Default::default(), 126 | floating_utilization: Default::default(), 127 | last_floating_debt_update: Default::default(), 128 | max_future_pools: Default::default(), 129 | fixed_pools: Default::default(), 130 | smart_pool_fee_rate: Default::default(), 131 | earnings_accumulator: Default::default(), 132 | last_accumulator_accrual: Default::default(), 133 | earnings_accumulator_smooth_factor: Default::default(), 134 | price_feed: Default::default(), 135 | listed: Default::default(), 136 | floating_full_utilization: Default::default(), 137 | floating_a: Default::default(), 138 | floating_b: Default::default(), 139 | floating_max_utilization: Default::default(), 140 | treasury_fee_rate: Default::default(), 141 | asset: Default::default(), 142 | base_market: false, 143 | symbol: Default::default(), 144 | } 145 | } 146 | 147 | pub fn contract( 148 | &self, 149 | client: Arc>, 150 | ) -> crate::generate_abi::market_protocol::MarketProtocol> { 151 | crate::generate_abi::market_protocol::MarketProtocol::new(self.contract, client) 152 | } 153 | 154 | pub fn total_assets(&self, timestamp: U256) -> U256 { 155 | let latest = ((timestamp - (timestamp % INTERVAL)) / INTERVAL).as_u32(); 156 | let mut smart_pool_earnings = U256::zero(); 157 | for i in latest..=latest + self.max_future_pools as u32 { 158 | let maturity = U256::from(INTERVAL * i); 159 | if let Some(fixed_pool) = self.fixed_pools.get(&maturity) { 160 | if maturity > fixed_pool.last_accrual { 161 | smart_pool_earnings += if timestamp < maturity { 162 | fixed_pool.unassigned_earnings.mul_div_down( 163 | timestamp - fixed_pool.last_accrual, 164 | maturity - fixed_pool.last_accrual, 165 | ) 166 | } else { 167 | fixed_pool.unassigned_earnings 168 | } 169 | } 170 | } 171 | } 172 | self.floating_assets 173 | + smart_pool_earnings 174 | + self.accumulated_earnings(timestamp) 175 | + (self.total_floating_borrow_assets(timestamp) - self.floating_debt) 176 | .mul_wad_down(U256::exp10(18) - self.treasury_fee_rate) 177 | } 178 | 179 | pub fn accumulated_earnings(&self, timestamp: U256) -> U256 { 180 | let elapsed = timestamp - self.last_accumulator_accrual; 181 | if elapsed > U256::zero() { 182 | self.earnings_accumulator.mul_div_down( 183 | elapsed, 184 | elapsed 185 | + self 186 | .earnings_accumulator_smooth_factor 187 | .mul_wad_down(U256::from(INTERVAL * self.max_future_pools as u32)), 188 | ) 189 | } else { 190 | U256::zero() 191 | } 192 | } 193 | 194 | fn floating_borrow_rate(&self, utilization: U256) -> U256 { 195 | (I256::from_raw( 196 | self.floating_a 197 | .div_wad_down(self.floating_max_utilization - utilization), 198 | ) + I256::from(self.floating_b)) 199 | .into_raw() 200 | } 201 | 202 | pub fn total_floating_borrow_assets(&self, timestamp: U256) -> U256 { 203 | let floating_utilization = if self.floating_assets > U256::zero() { 204 | self.floating_debt.div_wad_up(self.floating_assets) 205 | } else { 206 | U256::zero() 207 | }; 208 | let new_debt = self.floating_debt.mul_wad_down( 209 | self.floating_borrow_rate(floating_utilization) 210 | .mul_div_down( 211 | timestamp - self.last_floating_debt_update, 212 | U256::from(365 * 24 * 60 * 60), 213 | ), 214 | ); 215 | self.floating_debt + new_debt 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::U256; 2 | 3 | use crate::{config::Config, fixed_point_math::FixedPointMath}; 4 | 5 | #[derive(Clone, Copy, Default, Debug)] 6 | pub struct NetworkStatus { 7 | pub gas_price: Option, 8 | pub gas_used: Option, 9 | pub eth_price: U256, 10 | } 11 | 12 | pub trait NetworkActions { 13 | fn tx_cost(&self, data: &Network, status: NetworkStatus) -> U256; 14 | } 15 | 16 | pub struct Network { 17 | gas_price: U256, 18 | gas_used: U256, 19 | actions: Box, 20 | } 21 | 22 | impl Network { 23 | pub fn from_config(config: &Config) -> Self { 24 | Self { 25 | gas_price: config.gas_price, 26 | gas_used: config.gas_used, 27 | actions: Self::get_network_from(config), 28 | } 29 | } 30 | 31 | pub fn tx_cost(&self, status: NetworkStatus) -> U256 { 32 | self.actions.tx_cost(self, status) 33 | } 34 | 35 | pub fn default_gas_used(&self) -> U256 { 36 | self.gas_used 37 | } 38 | 39 | fn get_network_from(config: &Config) -> Box { 40 | match config.chain_id { 41 | 1 | 5 => Self::get_network::(), 42 | 10 => Box::new(Optimism { 43 | l1_gas_used: config.l1_gas_used, 44 | l1_gas_price: config.l1_gas_price, 45 | }), 46 | _ => panic!("Unknown network!"), 47 | } 48 | } 49 | 50 | fn get_network( 51 | ) -> Box { 52 | Box::::default() 53 | } 54 | } 55 | 56 | #[derive(Default)] 57 | struct Ethereum; 58 | 59 | #[derive(Default)] 60 | struct Optimism { 61 | l1_gas_used: U256, 62 | l1_gas_price: U256, 63 | } 64 | 65 | impl NetworkActions for Ethereum { 66 | fn tx_cost(&self, data: &Network, status: NetworkStatus) -> U256 { 67 | (status.gas_price.unwrap_or(data.gas_price) * status.gas_used.unwrap_or(data.gas_used)) 68 | .mul_wad_down(status.eth_price) 69 | } 70 | } 71 | 72 | impl NetworkActions for Optimism { 73 | fn tx_cost(&self, _: &Network, status: NetworkStatus) -> U256 { 74 | (self.l1_gas_price * self.l1_gas_used).mul_wad_down(status.eth_price) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/Liquidator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | pragma solidity 0.8.17; 3 | 4 | import { Test, stdJson } from "forge-std/Test.sol"; 5 | import { Market } from "@exactly-protocol/protocol/contracts/Market.sol"; 6 | import { Auditor } from "@exactly-protocol/protocol/contracts/Auditor.sol"; 7 | import { MockPriceFeed } from "@exactly-protocol/protocol/contracts/mocks/MockPriceFeed.sol"; 8 | import { Liquidator, IMarket, ERC20, ISwapRouter, PoolAddress } from "../contracts/Liquidator.sol"; 9 | 10 | contract LiquidatorTest is Test { 11 | using stdJson for string; 12 | 13 | address internal constant ALICE = address(0x420); 14 | address internal constant BOB = address(0x069); 15 | 16 | Liquidator internal liquidator; 17 | Market internal marketDAI; 18 | Market internal marketUSDC; 19 | Market internal marketWBTC; 20 | Market internal marketwstETH; 21 | Auditor internal auditor; 22 | address internal timelock; 23 | ERC20 internal usdc; 24 | ERC20 internal dai; 25 | ERC20 internal weth; 26 | ERC20 internal wstETH; 27 | ERC20 internal wbtc; 28 | 29 | function setUp() public { 30 | liquidator = new Liquidator( 31 | address(this), 32 | getAddress("UniswapV3Factory", ""), 33 | ISwapRouter(getAddress("UniswapV3Router", "")) 34 | ); 35 | 36 | dai = ERC20(getAddress("DAI", "node_modules/@exactly-protocol/protocol/")); 37 | usdc = ERC20(getAddress("USDC", "node_modules/@exactly-protocol/protocol/")); 38 | weth = ERC20(getAddress("WETH", "node_modules/@exactly-protocol/protocol/")); 39 | wbtc = ERC20(getAddress("WBTC", "node_modules/@exactly-protocol/protocol/")); 40 | wstETH = ERC20(getAddress("wstETH", "node_modules/@exactly-protocol/protocol/")); 41 | auditor = Auditor(getAddress("Auditor", "node_modules/@exactly-protocol/protocol/")); 42 | timelock = getAddress("TimelockController", "node_modules/@exactly-protocol/protocol/"); 43 | marketDAI = Market(getAddress("MarketDAI", "node_modules/@exactly-protocol/protocol/")); 44 | marketUSDC = Market(getAddress("MarketUSDC", "node_modules/@exactly-protocol/protocol/")); 45 | marketWBTC = Market(getAddress("MarketWBTC", "node_modules/@exactly-protocol/protocol/")); 46 | marketwstETH = Market(getAddress("MarketwstETH", "node_modules/@exactly-protocol/protocol/")); 47 | 48 | vm.label(ALICE, "alice"); 49 | if (address(dai) != address(0)) { 50 | vm.label( 51 | PoolAddress.computeAddress(liquidator.factory(), PoolAddress.getPoolKey(address(dai), address(usdc), 500)), 52 | "DAI/USDC/500" 53 | ); 54 | } 55 | } 56 | 57 | function testMultiMarketLiquidation() external { 58 | deal(address(dai), BOB, 100_000 ether); 59 | vm.startPrank(BOB); 60 | dai.approve(address(marketDAI), type(uint256).max); 61 | marketDAI.deposit(100_000 ether, BOB); 62 | vm.stopPrank(); 63 | 64 | deal(address(usdc), ALICE, 100_000e6); 65 | vm.startPrank(ALICE); 66 | usdc.approve(address(marketUSDC), type(uint256).max); 67 | marketUSDC.deposit(100_000e6, ALICE); 68 | auditor.enterMarket(marketUSDC); 69 | marketDAI.borrow(70_000 ether, ALICE, ALICE); 70 | vm.stopPrank(); 71 | 72 | vm.startPrank(timelock); 73 | auditor.setPriceFeed(marketDAI, new MockPriceFeed(18, 0.01 ether)); 74 | vm.stopPrank(); 75 | 76 | uint256 balanceBefore = usdc.balanceOf(address(liquidator)); 77 | liquidator.liquidate( 78 | IMarket(address(marketDAI)), 79 | IMarket(address(marketUSDC)), 80 | ALICE, 81 | 67_300 ether, 82 | address(0), 83 | 500, 84 | 0 85 | ); 86 | assertGt(usdc.balanceOf(address(liquidator)), balanceBefore); 87 | } 88 | 89 | function testSingleMarketLiquidation() external { 90 | deal(address(dai), BOB, 100_000 ether); 91 | vm.startPrank(BOB); 92 | dai.approve(address(marketDAI), type(uint256).max); 93 | marketDAI.deposit(100_000 ether, BOB); 94 | vm.stopPrank(); 95 | 96 | deal(address(usdc), ALICE, 100_000e6); 97 | vm.startPrank(ALICE); 98 | usdc.approve(address(marketUSDC), type(uint256).max); 99 | marketUSDC.deposit(100_000e6, ALICE); 100 | auditor.enterMarket(marketUSDC); 101 | marketDAI.borrow(70_000 ether, ALICE, ALICE); 102 | marketUSDC.borrow(2_000e6, ALICE, ALICE); 103 | vm.stopPrank(); 104 | 105 | vm.startPrank(timelock); 106 | auditor.setPriceFeed(marketDAI, new MockPriceFeed(18, 0.01 ether)); 107 | vm.stopPrank(); 108 | 109 | liquidator.liquidate( 110 | IMarket(address(marketUSDC)), 111 | IMarket(address(marketUSDC)), 112 | ALICE, 113 | 2_000e6, 114 | address(dai), 115 | 500, 116 | 0 117 | ); 118 | assertGt(usdc.balanceOf(address(liquidator)), 0); 119 | } 120 | 121 | function testDoubleSwapLiquidation() external { 122 | deal(address(usdc), BOB, 100_000e6); 123 | vm.startPrank(BOB); 124 | usdc.approve(address(marketUSDC), type(uint256).max); 125 | marketUSDC.deposit(100_000e6, BOB); 126 | vm.stopPrank(); 127 | 128 | deal(address(wstETH), ALICE, 100 ether); 129 | vm.startPrank(ALICE); 130 | wstETH.approve(address(marketwstETH), type(uint256).max); 131 | marketwstETH.deposit(100 ether, ALICE); 132 | auditor.enterMarket(marketwstETH); 133 | marketUSDC.borrow(60_000e6, ALICE, ALICE); 134 | vm.stopPrank(); 135 | 136 | vm.startPrank(timelock); 137 | auditor.setPriceFeed(marketUSDC, new MockPriceFeed(18, 0.01 ether)); 138 | vm.stopPrank(); 139 | 140 | uint256 balancewstETHBefore = wstETH.balanceOf(address(liquidator)); 141 | liquidator.liquidate( 142 | IMarket(address(marketUSDC)), 143 | IMarket(address(marketwstETH)), 144 | ALICE, 145 | 2_000e6, 146 | address(weth), 147 | 500, 148 | 500 149 | ); 150 | assertGt(wstETH.balanceOf(address(liquidator)), balancewstETHBefore); 151 | } 152 | 153 | function testReverseDoubleSwapLiquidation() external { 154 | deal(address(wstETH), BOB, 100 ether); 155 | vm.startPrank(BOB); 156 | wstETH.approve(address(marketwstETH), type(uint256).max); 157 | marketwstETH.deposit(100 ether, BOB); 158 | vm.stopPrank(); 159 | 160 | deal(address(usdc), ALICE, 100_000e6); 161 | vm.startPrank(ALICE); 162 | usdc.approve(address(marketUSDC), type(uint256).max); 163 | marketUSDC.deposit(100_000e6, ALICE); 164 | auditor.enterMarket(marketUSDC); 165 | marketwstETH.borrow(30 ether, ALICE, ALICE); 166 | vm.stopPrank(); 167 | 168 | vm.startPrank(timelock); 169 | auditor.setPriceFeed(marketwstETH, new MockPriceFeed(18, 2 ether)); 170 | vm.stopPrank(); 171 | 172 | uint256 balanceUSDCBefore = usdc.balanceOf(address(liquidator)); 173 | liquidator.liquidate( 174 | IMarket(address(marketwstETH)), 175 | IMarket(address(marketUSDC)), 176 | ALICE, 177 | 20 ether, 178 | address(weth), 179 | 500, 180 | 500 181 | ); 182 | assertGt(usdc.balanceOf(address(liquidator)), balanceUSDCBefore); 183 | } 184 | 185 | function testAnotherDoubleSwapLiquidation() external { 186 | deal(address(wstETH), BOB, 100 ether); 187 | vm.startPrank(BOB); 188 | wstETH.approve(address(marketwstETH), type(uint256).max); 189 | marketwstETH.deposit(100 ether, BOB); 190 | vm.stopPrank(); 191 | 192 | deal(address(wbtc), ALICE, 100 ether); 193 | vm.startPrank(ALICE); 194 | wbtc.approve(address(marketWBTC), type(uint256).max); 195 | marketWBTC.deposit(1e8, ALICE); 196 | auditor.enterMarket(marketWBTC); 197 | marketwstETH.borrow(6 ether, ALICE, ALICE); 198 | vm.stopPrank(); 199 | 200 | vm.startPrank(timelock); 201 | auditor.setPriceFeed(marketWBTC, new MockPriceFeed(18, 1 ether)); 202 | vm.stopPrank(); 203 | 204 | uint256 balanceWBTCBefore = wbtc.balanceOf(address(liquidator)); 205 | liquidator.liquidate( 206 | IMarket(address(marketwstETH)), 207 | IMarket(address(marketWBTC)), 208 | ALICE, 209 | 2 ether, 210 | address(weth), 211 | 500, 212 | 500 213 | ); 214 | assertGt(wbtc.balanceOf(address(liquidator)), balanceWBTCBefore); 215 | } 216 | 217 | function testSwap() external { 218 | deal(address(usdc), address(liquidator), 666_666e6); 219 | assertEq(dai.balanceOf(address(liquidator)), 0); 220 | assertEq(usdc.balanceOf(address(liquidator)), 666_666e6); 221 | 222 | liquidator.swap(usdc, 666_666e6, dai, 0, 500); 223 | 224 | assertGt(dai.balanceOf(address(liquidator)), 0); 225 | assertEq(usdc.balanceOf(address(liquidator)), 0); 226 | } 227 | 228 | function testTransfer() external { 229 | deal(address(usdc), address(liquidator), 666_666e6); 230 | assertEq(usdc.balanceOf(address(this)), 0); 231 | assertEq(usdc.balanceOf(address(liquidator)), 666_666e6); 232 | 233 | liquidator.transfer(usdc, address(this), 666_666e6); 234 | 235 | assertEq(usdc.balanceOf(address(this)), 666_666e6); 236 | assertEq(usdc.balanceOf(address(liquidator)), 0); 237 | } 238 | 239 | function testAuthority() external { 240 | vm.prank(ALICE); 241 | vm.expectRevert("UNAUTHORIZED"); 242 | liquidator.addCaller(ALICE); 243 | 244 | vm.prank(ALICE); 245 | vm.expectRevert("UNAUTHORIZED"); 246 | liquidator.liquidate(IMarket(address(0)), IMarket(address(0)), address(0), 0, address(0), 0, 0); 247 | 248 | liquidator.addCaller(ALICE); 249 | 250 | vm.prank(ALICE); 251 | vm.expectRevert("UNAUTHORIZED"); 252 | liquidator.addCaller(ALICE); 253 | 254 | vm.prank(ALICE); 255 | vm.expectRevert("UNAUTHORIZED"); 256 | liquidator.transfer(ERC20(address(0)), address(0), 0); 257 | 258 | vm.prank(ALICE); 259 | vm.expectRevert(); 260 | liquidator.liquidate(IMarket(address(0)), IMarket(address(0)), address(0), 0, address(0), 0, 0); 261 | } 262 | 263 | function getAddress(string memory name, string memory base) internal returns (address addr) { 264 | if (block.chainid == 31337) return address(0); 265 | 266 | string memory network; 267 | if (block.chainid == 1) network = "mainnet"; 268 | else if (block.chainid == 5) network = "goerli"; 269 | 270 | addr = vm.readFile(string.concat(base, "deployments/", network, "/", name, ".json")).readAddress(".address"); 271 | vm.label(addr, name); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "noEmit": true 8 | }, 9 | "include": ["**/*", "**/.eslintrc.js"], 10 | "exclude": ["node_modules"] 11 | } 12 | --------------------------------------------------------------------------------