├── .commitlintrc.json ├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── lint_pr.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── ContributionAgreement ├── Dockerfile ├── README.md ├── esbuild.config.mjs ├── package.json ├── src ├── app.ts ├── config │ ├── config.ts │ ├── env.ts │ ├── index.ts │ └── schema.ts ├── data │ ├── Balance.ts │ ├── CreditAccountData.ts │ ├── CreditManagerData.ts │ ├── MultiCall.ts │ ├── PriceOnDemand.ts │ ├── exceptions.ts │ └── index.ts ├── di.ts ├── errors │ ├── ErrorHandler.ts │ ├── TransactionRevertedError.ts │ └── index.ts ├── index.ts ├── log │ └── index.ts ├── services │ ├── AddressProviderService.ts │ ├── Client.ts │ ├── HealthCheckerService.ts │ ├── OracleServiceV3.ts │ ├── RedstoneServiceV3.ts │ ├── liquidate │ │ ├── AAVELiquidatorContract.ts │ │ ├── AbstractLiquidator.ts │ │ ├── BatchLiquidator.ts │ │ ├── GHOLiquidatorContract.ts │ │ ├── OptimisiticResults.ts │ │ ├── PartialLiquidatorContract.ts │ │ ├── SiloLiquidatorContract.ts │ │ ├── SingularFullLiquidator.ts │ │ ├── SingularLiquidator.ts │ │ ├── SingularPartialLiquidator.ts │ │ ├── factory.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── viem-types.ts │ ├── notifier │ │ ├── AlertBucket.ts │ │ ├── ConsoleNotifier.ts │ │ ├── TelegramNotifier.ts │ │ ├── factory.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ └── types.ts │ ├── output │ │ ├── BaseWriter.ts │ │ ├── consoleWriter.ts │ │ ├── factory.ts │ │ ├── fileWriter.ts │ │ ├── index.ts │ │ ├── restWriter.ts │ │ ├── s3Writer.ts │ │ └── types.ts │ ├── scanner │ │ ├── Scanner.ts │ │ └── index.ts │ └── swap │ │ ├── base.ts │ │ ├── factory.ts │ │ ├── index.ts │ │ ├── noop.ts │ │ ├── oneInch.ts │ │ ├── types.ts │ │ └── uniswap.ts ├── utils │ ├── bigint-serializer.ts │ ├── bigint-utils.ts │ ├── detect-network.ts │ ├── ethers-6-temp │ │ ├── pathfinder │ │ │ ├── balancerVault.ts │ │ │ ├── core.ts │ │ │ ├── index.ts │ │ │ ├── pathOptions.ts │ │ │ ├── pathfinder.ts │ │ │ └── viem-types.ts │ │ └── txparser │ │ │ ├── ERC20Parser.ts │ │ │ ├── MellowLrtVaultAdapterParser.ts │ │ │ ├── PendleRouterAdapterParser.ts │ │ │ ├── TxParserHelper.ts │ │ │ ├── aaveV2LendingPoolAdapterParser.ts │ │ │ ├── aaveV2WrappedATokenAdapterParser.ts │ │ │ ├── abstractParser.ts │ │ │ ├── addressProviderParser.ts │ │ │ ├── balancerV2VaultParser.ts │ │ │ ├── camelotV3AdapterParser.ts │ │ │ ├── compoundV2CTokenAdapterParser.ts │ │ │ ├── convexBaseRewardPoolAdapterParser.ts │ │ │ ├── convexBoosterAdapterParser.ts │ │ │ ├── convextRewardPoolParser.ts │ │ │ ├── creditFacadeParser.ts │ │ │ ├── creditManagerParser.ts │ │ │ ├── curveAdapterParser.ts │ │ │ ├── daiUsdsAdapterParser.ts │ │ │ ├── erc626AdapterParser.ts │ │ │ ├── iParser.ts │ │ │ ├── index.ts │ │ │ ├── lidoAdapterParser.ts │ │ │ ├── lidoSTETHParser.ts │ │ │ ├── poolParser.ts │ │ │ ├── priceOracleParser.ts │ │ │ ├── stakingRewardsAdapterParser.ts │ │ │ ├── txParser.ts │ │ │ ├── uniV2AdapterParser.ts │ │ │ ├── uniV3AdapterParser.ts │ │ │ ├── velodromeV2RouterAdapterParser.ts │ │ │ ├── wstETHAdapterParser.ts │ │ │ └── yearnV2AdapterParser.ts │ ├── etherscan.ts │ ├── formatters.ts │ ├── getLogsPaginated.ts │ ├── index.ts │ ├── retry.ts │ ├── simulateMulticall.ts │ ├── types.ts │ └── typescript.ts └── version.ts ├── tsconfig.build.json ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .git 3 | .husky 4 | .vscode 5 | build 6 | node_modules 7 | *.log 8 | .env -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@gearbox-protocol/eslint-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/lint_pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | main: 13 | name: Validate PR title 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: amannn/action-semantic-pull-request@v4 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Check PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | env: 8 | HUSKY: 0 9 | CI: true 10 | 11 | jobs: 12 | checks: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | cache: "yarn" 20 | node-version-file: ".nvmrc" 21 | - name: Perform checks 22 | run: | 23 | yarn install --frozen-lockfile 24 | yarn typecheck:ci 25 | yarn lint:ci 26 | yarn prettier:ci 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "next" 8 | - "beta" 9 | workflow_dispatch: 10 | 11 | env: 12 | HUSKY: 0 13 | CI: true 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Login to GitHub Container Registry 23 | uses: docker/login-action@v3 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.actor }} 27 | password: ${{ github.token }} 28 | 29 | - name: Semantic Release 30 | uses: cycjimmy/semantic-release-action@v4 31 | with: 32 | extra_plugins: | 33 | @codedependant/semantic-release-docker 34 | env: 35 | GITHUB_TOKEN: ${{ github.token }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.yarn/patches 2 | !.yarn/plugins 3 | !.yarn/releases 4 | !.yarn/sdks 5 | !.yarn/versions 6 | .DS_Store 7 | .env 8 | .env.* 9 | .history/ 10 | .idea/ 11 | .pnp.* 12 | .yarn/* 13 | artifacts/ 14 | build/ 15 | cache/ 16 | coverage* 17 | keys/* 18 | node_modules/ 19 | typechain/ 20 | 21 | /logs/*.json 22 | *.log 23 | *.tsbuildinfo 24 | .eslintcache 25 | .eslint.local.json 26 | 27 | # Optimistic output 28 | /output -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 80, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "singleQuote": false, 10 | "bracketSpacing": false, 11 | "explicitTypes": "always" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { 4 | "name": "main" 5 | }, 6 | { 7 | "name": "next", 8 | "channel": "next", 9 | "prerelease": "next" 10 | }, 11 | { 12 | "name": "beta", 13 | "channel": "beta", 14 | "prerelease": "beta" 15 | } 16 | ], 17 | "plugins": [ 18 | "@semantic-release/commit-analyzer", 19 | "@semantic-release/release-notes-generator", 20 | [ 21 | "@semantic-release/github", 22 | { 23 | "successComment": false, 24 | "failTitle": false 25 | } 26 | ], 27 | [ 28 | "@codedependant/semantic-release-docker", 29 | { 30 | "dockerTags": [ 31 | "{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", 32 | "{{version}}" 33 | ], 34 | "dockerArgs": { 35 | "PACKAGE_VERSION": "{{version}}" 36 | }, 37 | "dockerImage": "liquidator-v2", 38 | "dockerRegistry": "ghcr.io", 39 | "dockerProject": "gearbox-protocol", 40 | "dockerBuildQuiet": false, 41 | "dockerLogin": false 42 | } 43 | ] 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "nomicfoundation.hardhat-solidity", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Liquidator V2", 9 | "type": "node", 10 | "request": "launch", 11 | // Debug current file in VSCode 12 | "program": "${workspaceFolder}/src/index.ts", 13 | "envFile": "${workspaceFolder}/.env", 14 | /* 15 | * Path to tsx binary 16 | * Assuming locally installed 17 | */ 18 | "runtimeExecutable": "tsx", 19 | /* 20 | * Open terminal when debugging starts (Optional) 21 | * Useful to see console.logs 22 | */ 23 | // "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | // Files to exclude from debugger (e.g. call stack) 26 | "skipFiles": [ 27 | // Node.js internal core modules 28 | "/**" 29 | // Ignore all dependencies (optional) 30 | // "${workspaceFolder}/node_modules/**", 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | "eslint.validate": ["javascript", "typescript"], 9 | "files.eol": "\n", 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ContributionAgreement: -------------------------------------------------------------------------------- 1 | Gearbox Contribution Agreement 2 | 3 | 4 | Last Updated: May 13, 2023 5 | Version: 1.0 6 | 7 | 8 | PREAMBLE 9 | 10 | The purpose of this Contribution Agreement is to clearly define the terms under which the Intellectual Property Rights in and to the Contributions will be assigned. This will allow the Gearbox Foundation to properly collect and accumulate the Intellectual Property Rights to the Contributions and to defend the Gearbox protocol should there be a legal dispute regarding the intellectual property. 11 | 12 | This Contribution Agreement governs your contributions to the Repository. This Contribution Agreement shall have no prejudice to the licences governing the Repository Materials. 13 | 14 | By making any pull requests within the Repository, you hereby acknowledge that you have read, accept without modifications and agree to be bound by this Contribution Agreement. If you do not accept and agree to this Contribution Agreement, you shall not make any pull requests to the Repository, and you shall refrain from doing so. 15 | 16 | TERMS 17 | 18 | Definitions 19 | 20 | “Contribution” means a Work that was pulled to the Repository and approved for inclusion to the Repository. 21 | 22 | “Contribution Agreement” means this Gearbox Contribution Agreement as may be updated from time to time. 23 | 24 | “FOSS Licence” shall mean a free and open-source software licence that allows for editing, modifying, or reusing software’s source code, such as the General Public Licence, BSD licence, MIT licence, Apache licence, or any similar licence. 25 | 26 | “Gearbox Foundation” means the Gearbox Foundation registered under the laws of the Cayman Islands. 27 | 28 | “Intellectual Property Rights” shall mean all and any rights conferred under statute, common law, copyright or equity, now or in the future, including all proprietary rights, copyright and other rights in and to inventions, discoveries, ideas, improvements, know-how, trade secrets, trademarks and trade names, service marks, design rights, rights in get-up, database rights and rights in data, patents, mask works, utility models, goodwill and the right to sue for passing off or unfair competition, rights in domain names, all other intellectual property rights, in each case whether registered or unregistered and including all applications and rights to apply for and be granted, renewals or extensions of, and rights to claim priority from, such rights and all similar or equivalent rights or forms of protection which subsist or will subsist now or in the future in any part of the world, including, without limitation, the right to sue for and recover damages for past infringements. 29 | 30 | “Repository” means the Gearbox protocol Github repository available at https://github.com/Gearbox-protocol. 31 | 32 | “Repository Materials” means any copyrightable material contained in the Repository, such as, without limitation, computer software code, whether in human-readable or machine-executable form, non-functional data sets, documentation, audio files, video files, graphic image files, designs, and help files. 33 | 34 | “Work” means a copyrightable work of authorship, such as computer software code, whether in human-readable or machine-executable form, non-functional data sets, documentation, audio files, video files, graphic image files, designs, and help files, including a portion of a larger work, and a modification or addition to another work. 35 | 36 | “you” or “your” means the individual accepting this Contribution Agreement. 37 | 38 | Assignment 39 | 40 | Subject to section 4 below, you hereby irrevocably assign, convey, and transfer to the Gearbox Foundation all and any title, interest, and rights, including the Intellectual Property Rights, in and to the Contributions. 41 | 42 | The foregoing assignment and transfer includes, but is not limited to, the assignment and transfer of the rights to: use, exploit and utilise the Contributions by any means and in any way; allow others to use and prohibit others from using the Contributions; record, reproduce and copy the Contributions; distribute, monetise and commercialise one or more copies of the Contributions in any form or by any means, including on any tangible media; make available the Contributions to the general public by any means and in any form; rent, lease and licence the originals and copies of the Contributions; import and export the Contributions; publicly display the Contributions; translate in any language, adapt, modify and amend the Contributions without any limitations and restrictions; create derivative works based on the Contributions without any limitations and restrictions, and without obligation to pay you any remuneration in connection with the above; transfer, assign or otherwise alienate any and all rights in and to the Contributions; otherwise exploit, process or use the Contributions, without limitations, reservations or restrictions. 43 | 44 | The assignment of rights, title, and interest under this Contribution Agreement shall extend for the entire duration of the respective rights. In cases where the applicable law imposes restrictions on the assignment term, the duration of the assignment shall be limited to the longest period permitted by the applicable law. 45 | 46 | Subject to section 4 below, you hereby confirm that (i) the Contributions are created by you, (ii) as of immediately prior to the assignment and transfer under this section 2, you own all and any title, interest, and rights, including the Intellectual Property Rights, in and to the Contributions, and (iii) the Contributions do not infringe the rights, including Intellectual Property Rights, of any third party. 47 | 48 | You hereby acknowledge that the Contributions will become a part of the Repository Materials and may be used for any purpose, including, but not limited to, commercial purposes. 49 | 50 | You hereby acknowledge and agree that you shall not have the right to revoke the foregoing assignment and transfer of the title, interest, and rights, including the Intellectual Property Rights, in or to the Contributions. 51 | 52 | If you modify any part of the Repository Materials, but such modifications do not result in the creation of an individualised Work, the modifications shall not be deemed to be a derivative Work of the original Work and shall not be a Contribution. 53 | 54 | Moral Rights 55 | 56 | To the maximum extent permitted by applicable laws, you hereby waive and agree not to enforce all and any of your moral rights as that term is defined under the applicable law, with regard to the Contributions. 57 | 58 | In the event that a waiver of moral rights is not legally permissible, you agree to assume an obligation not to enforce these moral rights. 59 | 60 | You hereby understand and accept that your Contributions may be used, adapted, modified, or displayed without seeking your consent or providing attribution, and that you will not seek to assert any claim or legal action based on your moral rights in relation to the use of your Contributions. 61 | 62 | Free and Open-Source Works 63 | 64 | If a Work submitted by you to the Repository, or any component thereof, is governed by a FOSS Licence, you shall, before or when submitting the respective Work, (i) clearly identify the part of the Work that is subject to the FOSS Licence, (ii) provide the source of the original Work, (iii) provide the applicable FOSS Licence and applicable notices, including the copyright notices, and (iv) ensure that the Work and its intended use comply with the applicable FOSS Licence. 65 | 66 | Consideration 67 | 68 | Your consideration for entering into this Contribution Agreement and assigning and transferring the title, interest, and rights, including the Intellectual Property Rights, in the Contributions hereunder consists of the opportunity to participate in the development of the Gearbox protocol and the prospects of your Work being used or applied within it. You hereby acknowledge that these non-monetary benefits constitute valuable and sufficient consideration for your obligations and assignment of rights hereunder. 69 | 70 | Law and Jurisdiction 71 | 72 | The validity, interpretation, construction, and performance of this Contribution Agreement shall be governed by the laws of England and Wales, without giving effect to the principles of conflict of laws. 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.14 as dev 2 | 3 | ENV YARN_CACHE_FOLDER=/root/.yarn 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN --mount=type=cache,id=yarn,target=/root/.yarn \ 10 | yarn install --frozen-lockfile --ignore-engines \ 11 | && yarn build 12 | 13 | # Production npm modules 14 | 15 | FROM node:22.14 as prod 16 | 17 | ENV YARN_CACHE_FOLDER=/root/.yarn 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=dev /app/package.json /app 22 | COPY --from=dev /app/build/ /app/build 23 | 24 | RUN --mount=type=cache,id=yarn,target=/root/.yarn \ 25 | yarn install --production --frozen-lockfile --ignore-engines 26 | 27 | # Install foundy 28 | ENV FOUNDRY_DIR=/root/.foundry 29 | RUN mkdir ${FOUNDRY_DIR} && \ 30 | curl -L https://foundry.paradigm.xyz | bash && \ 31 | ${FOUNDRY_DIR}/bin/foundryup 32 | 33 | # Final image 34 | 35 | FROM gcr.io/distroless/nodejs22-debian12 36 | ARG PACKAGE_VERSION 37 | ENV PACKAGE_VERSION=${PACKAGE_VERSION:-dev} 38 | LABEL org.opencontainers.image.version="${PACKAGE_VERSION}" 39 | 40 | WORKDIR /app 41 | COPY --from=prod /app /app 42 | COPY --from=prod /root/.foundry/bin/cast /app 43 | CMD ["--enable-source-maps", "/app/build/index.mjs"] 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gearbox Liquidation node V2 2 | 3 | Liquidation node designed for V2 version. Liquidador uses Gearbox Smart Router to find optimal ways how to sell all assets into underlying one. 4 | 5 | ## Optimistic Liquidations 6 | 7 | Liqudator has special "OPTIMITIC" mode which is designed to predict potential problem with liqudation. Liquidation is run in this mode on fork net only, after running script which set all liquidation threshold to zero, what makes all credit account liquidatable. Then liquidator makes network state snapshot and liquidation account one by one, revetrting to saved snapshot each time liquidation was done. 8 | 9 | After testing all liquidation, it exists and save json file or send it on server via POST request. 10 | 11 | This mode increases protocol security showing potential problems with liquidations before they happened. 12 | 13 | ## How to configure 14 | 15 | Use environment variables to configure bot 16 | 17 | # Environment Variables Configuration 18 | 19 | | Environment Variable | Description | 20 | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | 21 | | `JSON_RPC_PROVIDERS` | RPC providers to use. | 22 | | `PRIVATE_KEY` | Private key used to send liquidation transactions. | 23 | | `SLIPPAGE` | Slippage value for pathfinder. | 24 | | `ADDRESS_PROVIDER` | By default uses address provider from @gearbox-protocol/sdk-gov. Use this option to override address provider. | 25 | | `HF_TRESHOLD` | Filter out all accounts with HF >= threshold during scan stage. | 26 | | `DEPLOY_AAVE_PARTIAL_LIQUIDATOR` | Deploy Aave partial liquidator contracts. | 27 | | `DEPLOY_GHO_PARTIAL_LIQUIDATOR` | Deploy Gho partial liquidator contracts. | 28 | | `AAVE_PARTIAL_LIQUIDATOR_ADDRESS` | Address of deployed partial liquidator contract for all credit managers except for GHO-based. | 29 | | `GHO_PARTIAL_LIQUIDATOR_ADDRESS` | Address of deployed partial liquidator contract for GHO credit managers. | 30 | | `DEPLOY_BATCH_LIQUIDATOR` | Deploy batch liquidator contracts. | 31 | | `BATCH_LIQUIDATOR_ADDRESS` | Address of deployed batch liquidator contract. | 32 | | `BATCH_SIZE` | Number of accounts to liquidate at once using batch liquidator. | 33 | | `MIN_BALANCE` | If balance drops before this value - we should send notification. | 34 | | `TELEGRAM_BOT_TOKEN` | Telegram bot token used to send notifications. | 35 | | `TELEGRAM_NOTIFICATIONS_CHANNEL` | Telegram channel where bot will post non-critical notifications. | 36 | | `TELEGRAM_ALERTS_CHANNEL` | Telegram channel where bot will post critical notifications. | 37 | | `RESTAKING_WORKAROUND` | Flag to enable less eager liquidations for LRT tokens. | 38 | | `SWAP_TO_ETH` | Use this mechanism to swap underlying token to ETH after the liquidation (abandoned feature). | 39 | | `ONE_INCH_API_KEY` | 1inch API key for swapper. | 40 | | `APP_NAME` | App name used in various messages to distinguish instances. | 41 | | `PORT` | Port to expose some vital signals and metrics. | 42 | | `DEBUG_ACCOUNTS` | Only check these accounts during local debug session. | 43 | | `DEBUG_MANAGERS` | Only check these credit managers during local debug session. | 44 | | `OPTIMISTIC` | Enable optimistic liquidations. | 45 | | `CAST_BIN` | Path to foundry/cast binary, so that we can create tree-like traces in case of errors. Used during optimistic liquidations. | 46 | | `OUT_DIR` | Directory to save JSON with optimistic liquidation results. | 47 | | `OUT_ENDPOINT` | REST endpoint to POST JSON with optimistic liquidation results. | 48 | | `OUT_HEADERS` | Headers for REST endpoint. | 49 | | `OUT_FILE_NAME` | Filename of JSON with optimistic liquidation results for S3 or dir output. | 50 | | `OUT_S3_BUCKET` | S3 bucket to upload JSON with optimistic liquidation results. | 51 | | `OUT_S3_PREFIX` | S3 bucket path prefix. | 52 | 53 | ## How to launch 54 | 55 | The liquidator is distributed as [docker image](https://github.com/Gearbox-protocol/liquidator-v2/pkgs/container/liquidator-v2) 56 | 57 | For example, write your config into `.env` file and then run: 58 | 59 | ```bash 60 | docker run --env-file .env ghcr.io/gearbox-protocol/liquidator-v2:latest 61 | ``` 62 | 63 | ### In normal mode 64 | 65 | Set required env variables. Do not enable `OPTIMISTIC_LIQUIDATIONS`, `OUT_*` variables are not required. 66 | 67 | ### In optimistic mode 68 | 69 | Set required env variables. Set `OPTIMISTIC_LIQUIDATIONS` to `true`, configure `OUT_*` variables for your desired output format. 70 | 71 | ### Important information for contributors 72 | 73 | As a contributor to the Gearbox Protocol GitHub repository, your pull requests indicate acceptance of our Gearbox Contribution Agreement. This agreement outlines that you assign the Intellectual Property Rights of your contributions to the Gearbox Foundation. This helps safeguard the Gearbox protocol and ensure the accumulation of its intellectual property. Contributions become part of the repository and may be used for various purposes, including commercial. As recognition for your expertise and work, you receive the opportunity to participate in the protocol's development and the potential to see your work integrated within it. The full Gearbox Contribution Agreement is accessible within the [repository](/ContributionAgreement) for comprehensive understanding. [Let's innovate together!] 74 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import { build } from "esbuild"; 2 | 3 | build({ 4 | entryPoints: ["src/index.ts"], 5 | outdir: "build", 6 | bundle: true, 7 | platform: "node", 8 | format: "esm", 9 | outExtension: { ".js": ".mjs" }, 10 | target: ["node20"], 11 | sourcemap: "external", 12 | banner: { 13 | js: ` 14 | import { createRequire } from 'module'; 15 | import { fileURLToPath } from 'url'; 16 | 17 | const require = createRequire(import.meta.url); 18 | const __filename = fileURLToPath(import.meta.url); 19 | const __dirname = path.dirname(__filename); 20 | `, 21 | }, 22 | external: ["node-pty"], 23 | }).catch(e => { 24 | console.error(e); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gearbox-protocol/liquidator-v2", 3 | "description": "Gearbox liquidation bot", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "private": true, 7 | "type": "module", 8 | "scripts": { 9 | "clean": "rm -rf build", 10 | "build": "node esbuild.config.mjs", 11 | "start": "tsx --env-file .env src/index.ts | pino-pretty --colorize", 12 | "prepare": "husky", 13 | "prettier": "prettier --write .", 14 | "prettier:ci": "npx prettier --check .", 15 | "lint": "eslint \"**/*.ts\" --fix", 16 | "lint:ci": "eslint \"**/*.ts\"", 17 | "typecheck:ci": "tsc --noEmit", 18 | "test": "vitest" 19 | }, 20 | "dependencies": { 21 | "node-pty": "^1.0.0", 22 | "pino-pretty": "^13.0.0" 23 | }, 24 | "devDependencies": { 25 | "@aws-sdk/client-s3": "^3.777.0", 26 | "@commitlint/cli": "^19.8.0", 27 | "@commitlint/config-conventional": "^19.8.0", 28 | "@flashbots/ethers-provider-bundle": "^1.0.0", 29 | "@gearbox-protocol/eslint-config": "2.0.0-next.2", 30 | "@gearbox-protocol/liquidator-v2-contracts": "^2.4.0", 31 | "@gearbox-protocol/prettier-config": "2.0.0", 32 | "@gearbox-protocol/sdk-gov": "2.34.0-next.112", 33 | "@gearbox-protocol/types": "^1.14.6", 34 | "@redstone-finance/evm-connector": "^0.7.3", 35 | "@types/node": "^22.13.11", 36 | "@uniswap/sdk-core": "^7.7.2", 37 | "@uniswap/v3-sdk": "^3.25.2", 38 | "@vlad-yakovlev/telegram-md": "^2.0.0", 39 | "abitype": "^1.0.8", 40 | "axios": "^1.8.4", 41 | "axios-retry": "^4.5.0", 42 | "date-fns": "^4.1.0", 43 | "di-at-home": "^0.0.7", 44 | "dotenv": "^16.4.7", 45 | "esbuild": "^0.25.2", 46 | "eslint": "^8.57.0", 47 | "ethers": "^6.13.5", 48 | "husky": "^9.1.7", 49 | "lint-staged": "^15.5.0", 50 | "nanoid": "^5.1.5", 51 | "pino": "^9.6.0", 52 | "prettier": "^3.5.3", 53 | "redstone-protocol": "^1.0.5", 54 | "tsx": "^4.19.3", 55 | "typescript": "^5.8.2", 56 | "viem": "^2.24.2", 57 | "vitest": "^3.0.9" 58 | }, 59 | "prettier": "@gearbox-protocol/prettier-config", 60 | "lint-staged": { 61 | "*.ts": [ 62 | "eslint --fix", 63 | "prettier --write" 64 | ], 65 | "*.{json,md}": "prettier --write" 66 | }, 67 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 68 | } 69 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config/index.js"; 2 | import { DI } from "./di.js"; 3 | import { type ILogger, Logger } from "./log/index.js"; 4 | import type { AddressProviderService } from "./services/AddressProviderService.js"; 5 | import type Client from "./services/Client.js"; 6 | import type HealthCheckerService from "./services/HealthCheckerService.js"; 7 | import type { IOptimisticOutputWriter } from "./services/output/index.js"; 8 | import type { RedstoneServiceV3 } from "./services/RedstoneServiceV3.js"; 9 | import type { Scanner } from "./services/scanner/index.js"; 10 | import type { ISwapper } from "./services/swap/index.js"; 11 | import version from "./version.js"; 12 | 13 | class App { 14 | @Logger("App") 15 | log!: ILogger; 16 | 17 | @DI.Inject(DI.Config) 18 | config!: Config; 19 | 20 | @DI.Inject(DI.AddressProvider) 21 | addressProvider!: AddressProviderService; 22 | 23 | @DI.Inject(DI.Scanner) 24 | scanner!: Scanner; 25 | 26 | @DI.Inject(DI.HealthChecker) 27 | healthChecker!: HealthCheckerService; 28 | 29 | @DI.Inject(DI.Redstone) 30 | redstone!: RedstoneServiceV3; 31 | 32 | @DI.Inject(DI.Client) 33 | client!: Client; 34 | 35 | @DI.Inject(DI.Output) 36 | outputWriter!: IOptimisticOutputWriter; 37 | 38 | @DI.Inject(DI.Swapper) 39 | swapper!: ISwapper; 40 | 41 | public async launch(): Promise { 42 | const msg = [ 43 | `Launching liquidator v${version}`, 44 | this.config.swapToEth ? `with swapping via ${this.config.swapToEth}` : "", 45 | "in", 46 | this.config.optimistic ? "optimistic" : "", 47 | this.config.liquidationMode, 48 | "mode", 49 | ] 50 | .filter(Boolean) 51 | .join(" "); 52 | this.log.info(msg); 53 | 54 | await this.client.launch(); 55 | await this.addressProvider.launch(); 56 | 57 | await this.redstone.launch(); 58 | 59 | this.healthChecker.launch(); 60 | await this.swapper.launch(this.config.network); 61 | await this.scanner.launch(); 62 | 63 | if (this.config.optimistic) { 64 | this.log.debug("optimistic liquidation finished, writing output"); 65 | await this.outputWriter.write(); 66 | this.log.debug("saved optimistic liquidation output, exiting"); 67 | process.exit(0); 68 | } 69 | } 70 | } 71 | 72 | export async function launchApp(): Promise { 73 | const config = await Config.load(); 74 | DI.set(DI.Config, config); 75 | const app = new App(); 76 | await app.launch(); 77 | } 78 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { createPublicClient, http } from "viem"; 3 | 4 | import { createClassFromType, detectNetwork } from "../utils/index.js"; 5 | import { envConfig } from "./env.js"; 6 | import type { PartialV300ConfigSchema } from "./schema.js"; 7 | import { ConfigSchema } from "./schema.js"; 8 | 9 | // These limits work for DRPC and Alchemy 10 | const PAGE_SIZE: Record = { 11 | Mainnet: 100_000n, 12 | Optimism: 500_000n, 13 | Arbitrum: 500_000n, 14 | Base: 500_000n, 15 | Sonic: 500_000n, 16 | }; 17 | 18 | interface DynamicConfig { 19 | readonly network: NetworkType; 20 | readonly chainId: number; 21 | readonly startBlock: bigint; 22 | readonly logsPageSize: bigint; 23 | } 24 | 25 | const ConfigClass = createClassFromType(); 26 | 27 | export class Config extends ConfigClass { 28 | static async load(): Promise { 29 | const schema = ConfigSchema.parse(envConfig); 30 | 31 | const client = createPublicClient({ 32 | transport: http(schema.ethProviderRpcs[0]), 33 | name: "detect network client", 34 | }); 35 | 36 | const [startBlock, chainId, network] = await Promise.all([ 37 | client.getBlockNumber(), 38 | client.getChainId(), 39 | detectNetwork(client), 40 | ]); 41 | return new Config({ 42 | ...partialLiquidatorsV300Defaults(network), 43 | ...schema, 44 | startBlock, 45 | chainId: Number(chainId), 46 | network, 47 | logsPageSize: schema.logsPageSize || PAGE_SIZE[network], 48 | }); 49 | } 50 | 51 | public get isPartial(): boolean { 52 | return this.liquidationMode === "partial"; 53 | } 54 | 55 | public get isBatch(): boolean { 56 | return this.liquidationMode === "batch"; 57 | } 58 | } 59 | 60 | /** 61 | * Returns env variables with addresses of pre-deployed partial liquidator contracts for v3.0 62 | * Credit managers v3.1 and router v3.1 and their partial liquidator contracts are deployed using create2, so no need to hardcode constants here 63 | * @param network 64 | * @param beta 65 | * @returns 66 | */ 67 | function partialLiquidatorsV300Defaults( 68 | network: NetworkType, 69 | ): PartialV300ConfigSchema { 70 | switch (network) { 71 | case "Mainnet": { 72 | return { 73 | aavePartialLiquidatorAddress: 74 | "0x0d394114fe3a40a39690b7951bf536de7e8fbf4b", 75 | ghoPartialLiquidatorAddress: 76 | "0x4c7c2b2115c392d98278ca7f2def992a08bb06f0", 77 | dolaPartialLiquidatorAddress: 78 | "0xc1f60b2f3d41bb15738dd52906cdc1de96825ef3", 79 | }; 80 | } 81 | case "Arbitrum": { 82 | return { 83 | aavePartialLiquidatorAddress: 84 | "0x7268d7017a330816c69d056ec2e64a8d2c954fc0", 85 | }; 86 | } 87 | case "Optimism": { 88 | return { 89 | aavePartialLiquidatorAddress: 90 | "0x8437432977ace20b4fc27f3317c3a4567909b44f", 91 | }; 92 | } 93 | default: 94 | return {}; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigSchema } from "./schema.js"; 2 | 3 | const envConfigMapping: Record = { 4 | addressProviderOverride: "ADDRESS_PROVIDER", 5 | appName: "APP_NAME", 6 | batchLiquidatorAddress: "BATCH_LIQUIDATOR_ADDRESS", 7 | debugAccounts: "DEBUG_ACCOUNTS", 8 | debugManagers: "DEBUG_MANAGERS", 9 | batchSize: "BATCH_SIZE", 10 | castBin: "CAST_BIN", 11 | liquidationMode: "LIQUIDATION_MODE", 12 | deployPartialLiquidatorContracts: "DEPLOY_PARTIAL_LIQUIDATOR", 13 | deployBatchLiquidatorContracts: "DEPLOY_BATCH_LIQUIDATOR", 14 | ethProviderRpcs: "JSON_RPC_PROVIDERS", 15 | logsPageSize: "LOGS_PAGE_SIZE", 16 | hfThreshold: "HF_TRESHOLD", 17 | restakingWorkaround: "RESTAKING_WORKAROUND", 18 | minBalance: "MIN_BALANCE", 19 | oneInchApiKey: "ONE_INCH_API_KEY", 20 | optimistic: "OPTIMISTIC", 21 | optimisticTimestamp: "OPTIMISTIC_TIMESTAMP", 22 | outDir: "OUT_DIR", 23 | outEndpoint: "OUT_ENDPOINT", 24 | outHeaders: "OUT_HEADERS", 25 | outFileName: "OUT_FILE_NAME", 26 | outS3Bucket: "OUT_S3_BUCKET", 27 | outS3Prefix: "OUT_S3_PREFIX", 28 | aavePartialLiquidatorAddress: "AAVE_PARTIAL_LIQUIDATOR_ADDRESS", 29 | ghoPartialLiquidatorAddress: "GHO_PARTIAL_LIQUIDATOR_ADDRESS", 30 | dolaPartialLiquidatorAddress: "DOLA_PARTIAL_LIQUIDATOR_ADDRESS", 31 | siloPartialLiquidatorAddress: "SILO_PARTIAL_LIQUIDATOR_ADDRESS", 32 | partialFallback: "PARTIAL_FALLBACK", 33 | privateKey: "PRIVATE_KEY", 34 | port: "PORT", 35 | slippage: "SLIPPAGE", 36 | swapToEth: "SWAP_TO_ETH", 37 | telegramBotToken: "TELEGRAM_BOT_TOKEN", 38 | telegramNotificationsChannel: "TELEGRAM_NOTIFICATIONS_CHANNEL", 39 | telegramAlersChannel: "TELEGRAM_ALERTS_CHANNEL", 40 | redstoneGateways: "REDSTONE_GATEWAYS", 41 | }; 42 | 43 | export const envConfig: Record = Object.fromEntries( 44 | Object.entries(envConfigMapping) 45 | .map(([f, k]) => { 46 | const keys = typeof k === "string" ? [k] : k; 47 | let value: string | undefined; 48 | for (const key of keys) { 49 | value = value ?? process.env[key]; 50 | } 51 | return [f, value]; 52 | }) 53 | .filter(([_, value]) => value !== undefined && value !== ""), 54 | ); 55 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { Config } from "./config.js"; 2 | -------------------------------------------------------------------------------- /src/config/schema.ts: -------------------------------------------------------------------------------- 1 | import { MAX_INT } from "@gearbox-protocol/sdk-gov"; 2 | import { Address } from "abitype/zod"; 3 | import { type Hex, isHex } from "viem"; 4 | import { z } from "zod"; 5 | 6 | const stringArrayLike = z 7 | .union([z.string(), z.array(z.string())]) 8 | .transform(v => (typeof v === "string" ? [v] : v)); 9 | 10 | const booleanLike = z 11 | .any() 12 | .transform(v => (typeof v === "string" ? v === "true" : Boolean(v))); 13 | 14 | const bigintLike = z.any().transform(v => BigInt(v)); 15 | 16 | export const PartialV300ConfigSchema = z.object({ 17 | /** 18 | * Address of deployed partial liquidator contract for all credit managers except for GHO- and DOLA- based 19 | */ 20 | aavePartialLiquidatorAddress: Address.optional(), 21 | /** 22 | * Address of deployed partial liquidator contract for GHO credit managers 23 | */ 24 | ghoPartialLiquidatorAddress: Address.optional(), 25 | /** 26 | * Address of deployed partial liquidator contract for DOLA credit managers 27 | */ 28 | dolaPartialLiquidatorAddress: Address.optional(), 29 | /** 30 | * Address of deployed partial liquidator contract for Sonic credit managers 31 | */ 32 | siloPartialLiquidatorAddress: Address.optional(), 33 | }); 34 | 35 | export type PartialV300ConfigSchema = z.infer; 36 | 37 | export const ConfigSchema = PartialV300ConfigSchema.extend({ 38 | /** 39 | * By default uses address provider from @gearbox-protocol/sdk-gov 40 | * Use this option to override address provider 41 | */ 42 | addressProviderOverride: Address.optional(), 43 | /** 44 | * App name used in various messages to distinguish instances 45 | */ 46 | appName: z.string().default("liquidator-ts"), 47 | /** 48 | * Port to expose some vital signals and metrics 49 | */ 50 | port: z.coerce.number().default(4000), 51 | /** 52 | * Only check these accounts during local debug session 53 | */ 54 | debugAccounts: stringArrayLike.optional().pipe(z.array(Address).optional()), 55 | /** 56 | * Only check these credit managers during local debug session 57 | */ 58 | debugManagers: stringArrayLike.optional().pipe(z.array(Address).optional()), 59 | /** 60 | * Path to foundry/cast binary, so that we can create tree-like traces in case of errors 61 | * Used during optimistic liquidations 62 | */ 63 | castBin: z.string().optional(), 64 | /** 65 | * RPC providers to use 66 | */ 67 | ethProviderRpcs: stringArrayLike 68 | .optional() 69 | .pipe(z.array(z.string().url()).min(1)), 70 | /** 71 | * Page size in blocks for eth_getLogs, default to some network-specific value 72 | */ 73 | logsPageSize: bigintLike.optional(), 74 | /** 75 | * Private key used to send liquidation transactions 76 | */ 77 | privateKey: z 78 | .string() 79 | .min(1) 80 | .transform((s): Hex => { 81 | return isHex(s) ? s : `0x${s}`; 82 | }), 83 | /** 84 | * If balance drops before this value - we should send notification 85 | */ 86 | minBalance: bigintLike 87 | .optional() 88 | .pipe(z.bigint().positive().default(500000000000000000n)), 89 | /** 90 | * Filter out all accounts with HF >= threshold during scan stage 91 | * 65535 is constant for zero-debt account (kind strang, because it's in the middle of the range of allowed values) 92 | */ 93 | hfThreshold: z.coerce.bigint().min(0n).max(MAX_INT).default(MAX_INT), 94 | /** 95 | * Enable optimistic liquidations 96 | */ 97 | optimistic: booleanLike.pipe(z.boolean().optional()), 98 | /** 99 | * Optimistic timestamp to pass from external runner, in ms 100 | */ 101 | optimisticTimestamp: z.coerce.number().int().positive().nullish(), 102 | /** 103 | * Override redstone gateways 104 | */ 105 | redstoneGateways: z 106 | .string() 107 | .optional() 108 | .transform(v => (v ? v.split(",") : undefined)), 109 | /** 110 | * Liquidator mode 111 | */ 112 | liquidationMode: z.enum(["full", "partial", "batch"]).default("full"), 113 | /** 114 | * The serive can deploy partial liquidator contracts. 115 | * Usage: deploy them once from local machine then pass the address to production service 116 | */ 117 | deployPartialLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), 118 | /** 119 | * Fallback to use full liquidator when partial liquidator fails 120 | */ 121 | partialFallback: booleanLike.pipe(z.boolean().optional()), 122 | /** 123 | * The serive can deploy partial liquidator contracts. 124 | * Usage: deploy them once from local machine then pass the address to production service 125 | */ 126 | deployBatchLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), 127 | /** 128 | * Address of deployed batch liquidator contract 129 | */ 130 | batchLiquidatorAddress: Address.default( 131 | "0x215c0962089fd52ac8e6a30261f86fb55dd81139", 132 | ), 133 | /** 134 | * Number of accounts to liquidate at once using batch liquidator 135 | */ 136 | batchSize: z.coerce.number().nonnegative().default(10), 137 | /** 138 | * Slippage value for pathfined 139 | */ 140 | slippage: z.coerce.number().min(0).max(10000).int().default(50), 141 | /** 142 | * Flag to enable less eager liquidations for LRT tokens 143 | */ 144 | restakingWorkaround: booleanLike.pipe(z.boolean().optional()), 145 | /** 146 | * Use this mechanism to swap underlying token to ETH after the liquidation (abandoned feature) 147 | */ 148 | swapToEth: z.enum(["1inch", "uniswap"]).optional(), 149 | /** 150 | * 1inch api key for swapper 151 | */ 152 | oneInchApiKey: z.string().optional(), 153 | 154 | /** 155 | * Directory to save json with optimistic liquidation results 156 | */ 157 | outDir: z.string().default("."), 158 | /** 159 | * REST endpoint to POST json with optimistic liquidation results 160 | */ 161 | outEndpoint: z.string().url().optional(), 162 | /** 163 | * Headers for REST endpoint 164 | */ 165 | outHeaders: z.string().default("{}"), 166 | /** 167 | * s3 bucket to upload json with optimistic liquidation results 168 | */ 169 | outS3Bucket: z.string().optional(), 170 | /** 171 | * s3 bucket path prefix 172 | */ 173 | outS3Prefix: z.string().default(""), 174 | /** 175 | * Filename of json with optimistic liquidation results for s3 or dir output 176 | */ 177 | outFileName: z.string().optional(), 178 | 179 | /** 180 | * Telegram bot token used to send notifications 181 | */ 182 | telegramBotToken: z.string().optional(), 183 | /** 184 | * Telegram channel where bot will post critical notifications 185 | */ 186 | telegramAlersChannel: z.string().startsWith("-").optional(), 187 | /** 188 | * Telegram channel where bot will post non-critical notifications 189 | */ 190 | telegramNotificationsChannel: z.string().startsWith("-").optional(), 191 | }); 192 | 193 | export type ConfigSchema = z.infer; 194 | -------------------------------------------------------------------------------- /src/data/Balance.ts: -------------------------------------------------------------------------------- 1 | import type { Address } from "viem"; 2 | 3 | export interface Balance { 4 | token: Address; 5 | balance: bigint; 6 | } 7 | -------------------------------------------------------------------------------- /src/data/CreditAccountData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PERCENTAGE_DECIMALS, 3 | tokenSymbolByAddress, 4 | } from "@gearbox-protocol/sdk-gov"; 5 | import type { Address } from "viem"; 6 | 7 | import type { Arrayish, Numberish } from "../utils/index.js"; 8 | 9 | export interface TokenBalanceRaw { 10 | token: string; 11 | balance: Numberish; 12 | isForbidden: boolean; 13 | isEnabled: boolean; 14 | isQuoted: boolean; 15 | quota: Numberish; 16 | quotaRate: Numberish; 17 | quotaCumulativeIndexLU: Numberish; 18 | } 19 | 20 | export interface CreditAccountDataRaw { 21 | isSuccessful: boolean; 22 | priceFeedsNeeded: Arrayish; 23 | addr: string; 24 | borrower: string; 25 | creditManager: string; 26 | cmName: string; 27 | creditFacade: string; 28 | underlying: string; 29 | debt: Numberish; 30 | cumulativeIndexLastUpdate: Numberish; 31 | cumulativeQuotaInterest: Numberish; 32 | accruedInterest: Numberish; 33 | accruedFees: Numberish; 34 | totalDebtUSD: Numberish; 35 | totalValue: Numberish; 36 | totalValueUSD: Numberish; 37 | twvUSD: Numberish; 38 | enabledTokensMask: Numberish; 39 | healthFactor: Numberish; 40 | baseBorrowRate: Numberish; 41 | aggregatedBorrowRate: Numberish; 42 | balances: Arrayish; 43 | since: Numberish; 44 | cfVersion: Numberish; 45 | expirationDate: Numberish; 46 | } 47 | 48 | export interface TokenBalance { 49 | token: Address; 50 | balance: bigint; 51 | isForbidden: boolean; 52 | isEnabled: boolean; 53 | isQuoted: boolean; 54 | quota: bigint; 55 | quotaRate: bigint; 56 | quotaCumulativeIndexLU: bigint; 57 | } 58 | 59 | export class CreditAccountData { 60 | readonly isSuccessful: boolean; 61 | readonly priceFeedsNeeded: Address[]; 62 | 63 | readonly addr: Address; 64 | readonly borrower: Address; 65 | readonly creditManager: Address; 66 | readonly creditFacade: Address; 67 | readonly underlyingToken: Address; 68 | readonly since: number; 69 | readonly expirationDate: number; 70 | readonly version: number; 71 | readonly cmName: string; 72 | 73 | readonly enabledTokenMask: bigint; 74 | readonly healthFactor: bigint; 75 | isDeleting: boolean; 76 | 77 | readonly borrowedAmount: bigint; 78 | readonly accruedInterest: bigint; 79 | readonly accruedFees: bigint; 80 | readonly totalDebtUSD: bigint; 81 | readonly borrowedAmountPlusInterestAndFees: bigint; 82 | readonly totalValue: bigint; 83 | readonly totalValueUSD: bigint; 84 | readonly twvUSD: bigint; 85 | 86 | readonly cumulativeIndexLastUpdate: bigint; 87 | readonly cumulativeQuotaInterest: bigint; 88 | 89 | readonly balances: Record = {}; 90 | readonly collateralTokens: Address[] = []; 91 | readonly allBalances: TokenBalance[] = []; 92 | 93 | constructor(payload: CreditAccountDataRaw) { 94 | this.isSuccessful = payload.isSuccessful; 95 | this.priceFeedsNeeded = [...payload.priceFeedsNeeded] as Address[]; 96 | 97 | this.addr = payload.addr.toLowerCase() as Address; 98 | this.borrower = payload.borrower.toLowerCase() as Address; 99 | this.creditManager = payload.creditManager.toLowerCase() as Address; 100 | this.creditFacade = payload.creditFacade.toLowerCase() as Address; 101 | this.underlyingToken = payload.underlying.toLowerCase() as Address; 102 | this.since = Number(payload.since); 103 | this.expirationDate = Number(payload.expirationDate); 104 | this.version = Number(payload.cfVersion); 105 | this.cmName = payload.cmName; 106 | 107 | this.healthFactor = BigInt(payload.healthFactor || 0n); 108 | this.enabledTokenMask = BigInt(payload.enabledTokensMask); 109 | this.isDeleting = false; 110 | 111 | this.borrowedAmount = BigInt(payload.debt); 112 | this.accruedInterest = BigInt(payload.accruedInterest || 0n); 113 | this.accruedFees = BigInt(payload.accruedFees || 0n); 114 | this.borrowedAmountPlusInterestAndFees = 115 | this.borrowedAmount + this.accruedInterest + this.accruedFees; 116 | this.totalDebtUSD = BigInt(payload.totalDebtUSD); 117 | this.totalValue = BigInt(payload.totalValue || 0n); 118 | this.totalValueUSD = BigInt(payload.totalValueUSD); 119 | this.twvUSD = BigInt(payload.twvUSD); 120 | 121 | this.cumulativeIndexLastUpdate = BigInt(payload.cumulativeIndexLastUpdate); 122 | this.cumulativeQuotaInterest = BigInt(payload.cumulativeQuotaInterest); 123 | 124 | payload.balances.forEach(b => { 125 | const token = b.token.toLowerCase() as Address; 126 | const balance: TokenBalance = { 127 | token, 128 | balance: BigInt(b.balance), 129 | isForbidden: b.isForbidden, 130 | isEnabled: b.isEnabled, 131 | isQuoted: b.isQuoted, 132 | quota: BigInt(b.quota), 133 | quotaRate: BigInt(b.quotaRate) * PERCENTAGE_DECIMALS, 134 | quotaCumulativeIndexLU: BigInt(b.quotaCumulativeIndexLU), 135 | }; 136 | 137 | if (!b.isForbidden) { 138 | this.balances[token] = balance.balance; 139 | this.collateralTokens.push(token); 140 | } 141 | 142 | this.allBalances.push(balance); 143 | }); 144 | } 145 | 146 | public get name(): string { 147 | return `${this.addr} of ${this.borrower} in ${this.managerName})`; 148 | } 149 | 150 | public get managerName(): string { 151 | const cmSymbol = tokenSymbolByAddress[this.underlyingToken]; 152 | return this.cmName || `${this.creditManager} (${cmSymbol})`; 153 | } 154 | 155 | public filterDust(): Record { 156 | const result: Record = {}; 157 | for (const { token, balance } of this.allBalances) { 158 | if (balance > 10n) { 159 | result[token] = balance; 160 | } 161 | } 162 | return result; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/data/CreditManagerData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PERCENTAGE_DECIMALS, 3 | PERCENTAGE_FACTOR, 4 | RAY, 5 | } from "@gearbox-protocol/sdk-gov"; 6 | import type { Address } from "viem"; 7 | 8 | import type { Arrayish, Numberish } from "../utils/index.js"; 9 | 10 | export interface CreditManagerDataRaw { 11 | addr: string; 12 | name: string; 13 | cfVersion: Numberish; 14 | creditFacade: string; 15 | creditConfigurator: string; 16 | underlying: string; 17 | pool: string; 18 | totalDebt: Numberish; 19 | totalDebtLimit: Numberish; 20 | baseBorrowRate: Numberish; 21 | minDebt: Numberish; 22 | maxDebt: Numberish; 23 | availableToBorrow: Numberish; 24 | collateralTokens: Arrayish; 25 | adapters: Arrayish<{ 26 | targetContract: string; 27 | adapter: string; 28 | }>; 29 | liquidationThresholds: Arrayish; 30 | isDegenMode: boolean; 31 | degenNFT: string; 32 | forbiddenTokenMask: Numberish; 33 | maxEnabledTokensLength: Numberish; 34 | feeInterest: Numberish; 35 | feeLiquidation: Numberish; 36 | liquidationDiscount: Numberish; 37 | feeLiquidationExpired: Numberish; 38 | liquidationDiscountExpired: Numberish; 39 | quotas: Arrayish<{ 40 | token: string; 41 | rate: Numberish; 42 | quotaIncreaseFee: Numberish; 43 | totalQuoted: Numberish; 44 | limit: Numberish; 45 | isActive: boolean; 46 | }>; 47 | lirm: { 48 | interestModel: string; 49 | version: Numberish; 50 | U_1: Numberish; 51 | U_2: Numberish; 52 | R_base: Numberish; 53 | R_slope1: Numberish; 54 | R_slope2: Numberish; 55 | R_slope3: Numberish; 56 | isBorrowingMoreU2Forbidden: boolean; 57 | }; 58 | isPaused: boolean; 59 | } 60 | 61 | export class CreditManagerData { 62 | readonly address: Address; 63 | readonly underlyingToken: Address; 64 | readonly pool: Address; 65 | readonly creditFacade: Address; 66 | readonly creditConfigurator: Address; 67 | readonly degenNFT: Address; 68 | readonly isDegenMode: boolean; 69 | readonly version: number; 70 | readonly isPaused: boolean; 71 | readonly forbiddenTokenMask: bigint; // V2 only: mask which forbids some particular tokens 72 | readonly name: string; 73 | 74 | readonly baseBorrowRate: number; 75 | 76 | readonly minDebt: bigint; 77 | readonly maxDebt: bigint; 78 | readonly availableToBorrow: bigint; 79 | readonly totalDebt: bigint; 80 | readonly totalDebtLimit: bigint; 81 | 82 | readonly feeInterest: number; 83 | readonly feeLiquidation: number; 84 | readonly liquidationDiscount: number; 85 | readonly feeLiquidationExpired: number; 86 | readonly liquidationDiscountExpired: number; 87 | 88 | readonly collateralTokens: Address[] = []; 89 | readonly supportedTokens: Record = {}; 90 | readonly adapters: Record; 91 | readonly contractsByAdapter: Record; 92 | readonly liquidationThresholds: Record = {}; 93 | 94 | constructor(payload: CreditManagerDataRaw) { 95 | this.address = payload.addr.toLowerCase() as Address; 96 | this.underlyingToken = payload.underlying.toLowerCase() as Address; 97 | this.name = payload.name; 98 | this.pool = payload.pool.toLowerCase() as Address; 99 | this.creditFacade = payload.creditFacade.toLowerCase() as Address; 100 | this.creditConfigurator = 101 | payload.creditConfigurator.toLowerCase() as Address; 102 | this.degenNFT = payload.degenNFT.toLowerCase() as Address; 103 | this.isDegenMode = payload.isDegenMode; 104 | this.version = Number(payload.cfVersion); 105 | this.isPaused = payload.isPaused; 106 | this.forbiddenTokenMask = BigInt(payload.forbiddenTokenMask); 107 | 108 | this.baseBorrowRate = Number( 109 | (BigInt(payload.baseBorrowRate) * 110 | (BigInt(payload.feeInterest) + PERCENTAGE_FACTOR) * 111 | PERCENTAGE_DECIMALS) / 112 | RAY, 113 | ); 114 | 115 | this.minDebt = BigInt(payload.minDebt); 116 | this.maxDebt = BigInt(payload.maxDebt); 117 | this.availableToBorrow = BigInt(payload.availableToBorrow); 118 | this.totalDebt = BigInt(payload.totalDebt); 119 | this.totalDebtLimit = BigInt(payload.totalDebtLimit); 120 | 121 | this.feeInterest = Number(payload.feeInterest); 122 | this.feeLiquidation = Number(payload.feeLiquidation); 123 | this.liquidationDiscount = Number(payload.liquidationDiscount); 124 | this.feeLiquidationExpired = Number(payload.feeLiquidationExpired); 125 | this.liquidationDiscountExpired = Number( 126 | payload.liquidationDiscountExpired, 127 | ); 128 | 129 | payload.collateralTokens.forEach(t => { 130 | const tLc = t.toLowerCase() as Address; 131 | 132 | this.collateralTokens.push(tLc); 133 | this.supportedTokens[tLc] = true; 134 | }); 135 | 136 | this.adapters = Object.fromEntries( 137 | payload.adapters.map(a => [ 138 | a.targetContract.toLowerCase() as Address, 139 | a.adapter.toLowerCase() as Address, 140 | ]), 141 | ); 142 | 143 | this.contractsByAdapter = Object.fromEntries( 144 | payload.adapters.map(a => [ 145 | a.adapter.toLowerCase() as Address, 146 | a.targetContract.toLowerCase() as Address, 147 | ]), 148 | ); 149 | 150 | for (let i = 0; i < payload.liquidationThresholds.length; i++) { 151 | const threshold = payload.liquidationThresholds[i]; 152 | const address = payload.collateralTokens[i]?.toLowerCase() as Address; 153 | if (address) { 154 | this.liquidationThresholds[address] = threshold; 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/data/MultiCall.ts: -------------------------------------------------------------------------------- 1 | export interface MultiCall { 2 | target: `0x${string}`; 3 | callData: `0x${string}`; 4 | } 5 | -------------------------------------------------------------------------------- /src/data/PriceOnDemand.ts: -------------------------------------------------------------------------------- 1 | export interface PriceOnDemand { 2 | token: `0x${string}`; 3 | callData: `0x${string}`; 4 | reserve: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/data/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | iExceptionsAbi, 3 | ilpPriceFeedExceptionsAbi, 4 | iRedstoneErrorsAbi, 5 | iRedstonePriceFeedExceptionsAbi, 6 | iRouterV3ErrorsAbi, 7 | } from "@gearbox-protocol/types/abi"; 8 | 9 | export const exceptionsAbis = [ 10 | ...iExceptionsAbi, 11 | ...iRedstonePriceFeedExceptionsAbi, 12 | ...iRedstoneErrorsAbi, 13 | ...ilpPriceFeedExceptionsAbi, 14 | ...iRouterV3ErrorsAbi, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Balance.js"; 2 | export * from "./CreditAccountData.js"; 3 | export * from "./CreditManagerData.js"; 4 | export * from "./exceptions.js"; 5 | export * from "./MultiCall.js"; 6 | export * from "./PriceOnDemand.js"; 7 | -------------------------------------------------------------------------------- /src/di.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInstance } from "di-at-home"; 2 | 3 | const Injectables = { 4 | AddressProvider: "AddressProvider", 5 | Client: "Client", 6 | Config: "Config", 7 | Docker: "Docker", 8 | HealthChecker: "HealthChecker", 9 | Liquidator: "Liquidator", 10 | Logger: "Logger", 11 | Notifier: "Notifier", 12 | OptimisticResults: "OptimisticResults", 13 | Oracle: "Oracle", 14 | Output: "Output", 15 | Redstone: "Redstone", 16 | Scanner: "Scanner", 17 | Swapper: "Swapper", 18 | } as const; 19 | 20 | export const DI = Object.assign( 21 | new ContainerInstance<{ 22 | AddressProvider: []; 23 | Client: []; 24 | Config: []; 25 | Docker: []; 26 | HealthChecker: []; 27 | Liquidator: []; 28 | Logger: [string]; 29 | Notifier: []; 30 | OptimisticResults: []; 31 | Oracle: []; 32 | Output: []; 33 | Redstone: []; 34 | Scanner: []; 35 | Swapper: []; 36 | }>(), 37 | Injectables, 38 | ); 39 | -------------------------------------------------------------------------------- /src/errors/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import events from "node:events"; 2 | import { createWriteStream, writeFileSync } from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { nanoid } from "nanoid"; 6 | import { spawn } from "node-pty"; 7 | import { 8 | BaseError, 9 | ContractFunctionExecutionError, 10 | encodeFunctionData, 11 | } from "viem"; 12 | 13 | import type { Config } from "../config/index.js"; 14 | import type { CreditAccountData } from "../data/index.js"; 15 | import type { ILogger } from "../log/index.js"; 16 | import { json_stringify } from "../utils/index.js"; 17 | import { TransactionRevertedError } from "./TransactionRevertedError.js"; 18 | 19 | export interface ExplainedError { 20 | errorJson?: string; 21 | shortMessage: string; 22 | longMessage: string; 23 | traceFile?: string; 24 | } 25 | 26 | export class ErrorHandler { 27 | log: ILogger; 28 | config: Config; 29 | 30 | constructor(config: Config, log: ILogger) { 31 | this.config = config; 32 | this.log = log; 33 | } 34 | 35 | public async explain( 36 | error: unknown, 37 | context?: CreditAccountData, 38 | saveTrace?: boolean, 39 | ): Promise { 40 | const logger = this.#caLogger(context); 41 | 42 | if (error instanceof BaseError) { 43 | const errorJson = `${nanoid()}.json`; 44 | const errorFile = path.resolve(this.config.outDir, errorJson); 45 | const asStr = json_stringify(error); 46 | writeFileSync(errorFile, asStr, "utf-8"); 47 | logger.debug(`saved original error to ${errorFile}`); 48 | 49 | let traceFile: string | undefined; 50 | if (saveTrace) { 51 | traceFile = await this.#saveErrorTrace(error, context); 52 | } 53 | 54 | return { 55 | errorJson, 56 | traceFile, 57 | shortMessage: error.shortMessage, 58 | longMessage: error.message, 59 | }; 60 | } 61 | const longMessage = error instanceof Error ? error.message : `${error}`; 62 | const shortMessage = longMessage.split("\n")[0].slice(0, 128); 63 | return { 64 | longMessage, 65 | shortMessage, 66 | }; 67 | } 68 | 69 | public async saveTransactionTrace(hash: string): Promise { 70 | return this.#runCast([ 71 | "run", 72 | "--rpc-url", 73 | this.config.ethProviderRpcs[0], 74 | hash, 75 | ]); 76 | } 77 | 78 | /** 79 | * Safely tries to save trace of failed transaction to configured output 80 | * @param error 81 | * @returns 82 | */ 83 | async #saveErrorTrace( 84 | e: BaseError, 85 | context?: CreditAccountData, 86 | ): Promise { 87 | let cast: string[] = []; 88 | if (e instanceof TransactionRevertedError) { 89 | cast = [ 90 | "run", 91 | "--rpc-url", 92 | this.config.ethProviderRpcs[0], 93 | e.receipt.transactionHash, 94 | ]; 95 | } else { 96 | const exErr = e.walk( 97 | err => err instanceof ContractFunctionExecutionError, 98 | ); 99 | if ( 100 | exErr instanceof ContractFunctionExecutionError && 101 | exErr.contractAddress 102 | ) { 103 | const data = encodeFunctionData({ 104 | abi: exErr.abi, 105 | args: exErr.args, 106 | functionName: exErr.functionName, 107 | }); 108 | cast = [ 109 | "call", 110 | "--trace", 111 | "--rpc-url", 112 | this.config.ethProviderRpcs[0], 113 | exErr.contractAddress, 114 | data, 115 | ]; 116 | } 117 | } 118 | if (!cast.length) { 119 | return undefined; 120 | } 121 | return this.#runCast(cast, context); 122 | } 123 | 124 | /** 125 | * Runs cast cli command and saves output to a unique file 126 | * @param args 127 | * @param context 128 | * @returns 129 | */ 130 | async #runCast( 131 | args: string[], 132 | context?: CreditAccountData, 133 | ): Promise { 134 | if (!this.config.castBin || !this.config.outDir) { 135 | return undefined; 136 | } 137 | 138 | const logger = this.#caLogger(context); 139 | try { 140 | const traceId = `${nanoid()}.trace`; 141 | const traceFile = path.resolve(this.config.outDir, traceId); 142 | const out = createWriteStream(traceFile, "utf-8"); 143 | await events.once(out, "open"); 144 | // use node-pty instead of node:child_process to have colored output 145 | const pty = spawn(this.config.castBin, args, { cols: 1024 }); 146 | pty.onData(data => out.write(data)); 147 | await new Promise(resolve => { 148 | pty.onExit(() => resolve(undefined)); 149 | }); 150 | logger.debug(`saved trace file: ${traceFile}`); 151 | return traceId; 152 | } catch (e) { 153 | logger.warn(`failed to save trace: ${e}`); 154 | } 155 | } 156 | 157 | #caLogger(ca?: CreditAccountData): ILogger { 158 | return ca 159 | ? this.log.child({ 160 | account: ca.addr, 161 | manager: ca.managerName, 162 | }) 163 | : this.log; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/errors/TransactionRevertedError.ts: -------------------------------------------------------------------------------- 1 | import type { TransactionReceipt } from "viem"; 2 | import { BaseError } from "viem"; 3 | 4 | export class TransactionRevertedError extends BaseError { 5 | override name = "TransactionRevertedError"; 6 | public readonly receipt: TransactionReceipt; 7 | 8 | constructor(receipt: TransactionReceipt) { 9 | super(`transaction ${receipt.transactionHash} reverted`, { 10 | metaMessages: [], 11 | }); 12 | this.receipt = receipt; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ErrorHandler.js"; 2 | export * from "./TransactionRevertedError.js"; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // These imports are required to establish correct order of dependency injections 2 | import "./services/AddressProviderService.js"; 3 | import "./services/Client.js"; 4 | import "./services/HealthCheckerService.js"; 5 | import "./services/OracleServiceV3.js"; 6 | import "./services/RedstoneServiceV3.js"; 7 | import "./services/scanner/index.js"; 8 | import "./services/liquidate/index.js"; 9 | import "./services/output/index.js"; 10 | import "./services/notifier/index.js"; 11 | import "./services/swap/index.js"; 12 | 13 | import { launchApp } from "./app.js"; 14 | 15 | Error.stackTraceLimit = Infinity; 16 | 17 | process.on("uncaughtException", e => { 18 | console.error(e); 19 | process.exit(1); 20 | }); 21 | 22 | process.on("unhandledRejection", e => { 23 | console.error(e); 24 | process.exit(1); 25 | }); 26 | 27 | launchApp().catch(e => { 28 | console.error("Cant start liquidator", e); 29 | process.exit(1); // exit code is easily visible for killled docker containers and ecs services 30 | }); 31 | -------------------------------------------------------------------------------- /src/log/index.ts: -------------------------------------------------------------------------------- 1 | import type { IFactory } from "di-at-home"; 2 | import type { Logger as ILogger } from "pino"; 3 | import { pino } from "pino"; 4 | 5 | import { DI } from "../di.js"; 6 | 7 | @DI.Factory(DI.Logger) 8 | class LoggerFactory implements IFactory { 9 | #logger: ILogger; 10 | 11 | constructor() { 12 | const executionId = process.env.EXECUTION_ID?.split(":").pop(); 13 | this.#logger = pino({ 14 | level: process.env.LOG_LEVEL ?? "debug", 15 | base: { executionId }, 16 | formatters: { 17 | level: label => { 18 | return { 19 | level: label, 20 | }; 21 | }, 22 | }, 23 | // fluent-bit (which is used in our ecs setup with loki) cannot handle unix epoch in millis out of the box 24 | timestamp: () => `,"time":${Date.now() / 1000.0}`, 25 | }); 26 | } 27 | 28 | public produce(name: string): ILogger { 29 | return this.#logger.child({ name }); 30 | } 31 | } 32 | 33 | export const Logger = (name: string) => DI.Transient(DI.Logger, name); 34 | 35 | export type { Logger as ILogger } from "pino"; 36 | -------------------------------------------------------------------------------- /src/services/AddressProviderService.ts: -------------------------------------------------------------------------------- 1 | import type { Address, NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { ADDRESS_PROVIDER } from "@gearbox-protocol/sdk-gov"; 3 | import { iAddressProviderV3Abi } from "@gearbox-protocol/types/abi"; 4 | import { getAbiItem, hexToString, stringToHex } from "viem"; 5 | 6 | import type { Config } from "../config/index.js"; 7 | import { DI } from "../di.js"; 8 | import { type ILogger, Logger } from "../log/index.js"; 9 | import { TxParser } from "../utils/ethers-6-temp/txparser/index.js"; 10 | import { getLogsPaginated } from "../utils/getLogsPaginated.js"; 11 | import type Client from "./Client.js"; 12 | 13 | const AP_SERVICES = [ 14 | "PRICE_ORACLE", 15 | "DATA_COMPRESSOR", 16 | "ROUTER", 17 | "PARTIAL_LIQUIDATION_BOT", 18 | "ACL", 19 | "DEGEN_DISTRIBUTOR", 20 | ] as const; 21 | 22 | export type AddressProviderKey = (typeof AP_SERVICES)[number]; 23 | 24 | const AP_BLOCK_BY_NETWORK: Record = { 25 | Mainnet: 18433056n, 26 | Arbitrum: 184650310n, 27 | Optimism: 118410000n, 28 | Base: 12299805n, // arbitrary block, NOT_DEPLOYED yet 29 | Sonic: 9779379n, 30 | }; 31 | 32 | @DI.Injectable(DI.AddressProvider) 33 | export class AddressProviderService { 34 | @Logger("AddressProvider") 35 | log!: ILogger; 36 | 37 | @DI.Inject(DI.Config) 38 | config!: Config; 39 | 40 | @DI.Inject(DI.Client) 41 | client!: Client; 42 | 43 | #addresses: Map> = new Map(); 44 | 45 | public async launch(): Promise { 46 | const address = 47 | this.config.addressProviderOverride ?? 48 | ADDRESS_PROVIDER[this.config.network]; 49 | const overrideS = this.config.addressProviderOverride 50 | ? ` (overrides default ${ADDRESS_PROVIDER[this.config.network]})` 51 | : ""; 52 | 53 | const toBlock = await this.client.pub.getBlockNumber(); 54 | 55 | const logs = await getLogsPaginated(this.client.logs, { 56 | address, 57 | event: getAbiItem({ abi: iAddressProviderV3Abi, name: "SetAddress" }), 58 | args: { key: AP_SERVICES.map(s => stringToHex(s, { size: 32 })) }, 59 | fromBlock: AP_BLOCK_BY_NETWORK[this.config.network], 60 | toBlock, 61 | strict: true, 62 | pageSize: this.config.logsPageSize, 63 | }); 64 | 65 | for (const { args } of logs) { 66 | const { key, version, value } = args; 67 | const service = hexToString(key!, { size: 32 }) as AddressProviderKey; 68 | const versions = this.#addresses.get(service) ?? new Map(); 69 | versions.set(Number(version!), value!); 70 | this.#addresses.set(service, versions); 71 | } 72 | 73 | // TODO: TxParser is really old and weird class, until we refactor it it's the best place to have this 74 | TxParser.addAddressProvider(address); 75 | 76 | this.log.info( 77 | `Launched on ${this.config.network} (${this.config.chainId}) using address provider ${address}${overrideS} with ${logs.length} entries`, 78 | ); 79 | } 80 | 81 | public findService( 82 | service: AddressProviderKey, 83 | minVersion = 0, 84 | maxVersion_?: number, 85 | ): Address { 86 | // defaults to same version for single-digit versions 87 | // or to same major version for 3-digit versions 88 | const maxVersion = 89 | maxVersion_ ?? 90 | (minVersion < 100 ? minVersion : Math.floor(minVersion / 100) * 100 + 99); 91 | this.log.debug( 92 | `looking for ${service} in version range [${minVersion}, ${maxVersion}]`, 93 | ); 94 | 95 | const versions = this.#addresses.get(service); 96 | if (!versions) { 97 | throw new Error(`cannot find latest ${service}: not entries at all`); 98 | } 99 | let version = minVersion; 100 | let address: Address | undefined; 101 | for (const [v, addr] of versions.entries()) { 102 | if (v >= version && v <= maxVersion) { 103 | version = v; 104 | address = addr; 105 | } 106 | } 107 | 108 | if (!address) { 109 | throw new Error(`cannot find latest ${service}`); 110 | } 111 | this.log.debug(`latest version of ${service}: v${version} at ${address}`); 112 | 113 | return address; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/services/HealthCheckerService.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | 3 | import { customAlphabet } from "nanoid"; 4 | 5 | import type { Config } from "../config/index.js"; 6 | import { DI } from "../di.js"; 7 | import type { ILogger } from "../log/index.js"; 8 | import { Logger } from "../log/index.js"; 9 | import version from "../version.js"; 10 | import type { Scanner } from "./scanner/index.js"; 11 | 12 | const nanoid = customAlphabet("1234567890abcdef", 8); 13 | 14 | @DI.Injectable(DI.HealthChecker) 15 | export default class HealthCheckerService { 16 | @Logger("HealthChecker") 17 | log!: ILogger; 18 | 19 | @DI.Inject(DI.Scanner) 20 | scanner!: Scanner; 21 | 22 | @DI.Inject(DI.Config) 23 | config!: Config; 24 | 25 | #start = Math.round(new Date().valueOf() / 1000); 26 | #id = nanoid(); 27 | 28 | /** 29 | * Launches health checker - simple web server 30 | */ 31 | public launch(): void { 32 | if (this.config.optimistic) { 33 | return; 34 | } 35 | 36 | const server = createServer(async (req, res) => { 37 | // Routing 38 | if (req.url === "/") { 39 | res.writeHead(200, { "Content-Type": "application/json" }); 40 | res.end( 41 | JSON.stringify({ 42 | start_time: this.#start, 43 | block_number: this.scanner.lastUpdated, 44 | version, 45 | }), 46 | ); 47 | } else if (req.url === "/metrics") { 48 | try { 49 | res.writeHead(200, { "Content-Type": "text/plain" }); 50 | res.end(this.#metrics()); 51 | } catch (ex) { 52 | res.writeHead(500, { "Content-Type": "text/plain" }); 53 | res.end("error"); 54 | } 55 | } else { 56 | res.writeHead(404, { "Content-Type": "text/plain" }); 57 | res.end("not found"); 58 | } 59 | }); 60 | 61 | const host = "0.0.0.0"; 62 | server.listen({ host, port: this.config.port }, () => { 63 | this.log.debug(`listening on ${host}:${this.config.port}`); 64 | }); 65 | server.on("error", e => { 66 | this.log.error(e); 67 | }); 68 | server.unref(); 69 | 70 | process.on("SIGTERM", () => { 71 | this.log.info("terminating"); 72 | server.close(); 73 | }); 74 | 75 | this.log.info("launched"); 76 | } 77 | 78 | /** 79 | * Returns metrics in prometheus format 80 | * https://prometheus.io/docs/concepts/data_model/ 81 | */ 82 | #metrics(): string { 83 | const labels = Object.entries({ 84 | instance_id: this.#id, 85 | network: this.config.network.toLowerCase(), 86 | version, 87 | }) 88 | .map(([k, v]) => `${k}="${v}"`) 89 | .join(", "); 90 | return `# HELP service_up Simple binary flag to indicate being alive 91 | # TYPE service_up gauge 92 | service_up{${labels}} 1 93 | 94 | # HELP start_time Start time, in unixtime 95 | # TYPE start_time gauge 96 | start_time{${labels}} ${this.#start} 97 | 98 | # HELP block_number Latest processed block 99 | # TYPE block_number gauge 100 | block_number{${labels}} ${this.scanner.lastUpdated} 101 | 102 | `; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/services/liquidate/AAVELiquidatorContract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aaveFlTakerAbi, 3 | aaveLiquidatorAbi, 4 | } from "@gearbox-protocol/liquidator-v2-contracts/abi"; 5 | import { 6 | AaveFLTaker_bytecode, 7 | AaveLiquidator_bytecode, 8 | } from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; 9 | import { contractsByNetwork } from "@gearbox-protocol/sdk-gov"; 10 | import type { Address } from "viem"; 11 | 12 | import type { ILogger } from "../../log/index.js"; 13 | import { Logger } from "../../log/index.js"; 14 | import PartialLiquidatorContract from "./PartialLiquidatorContract.js"; 15 | 16 | export default class AAVELiquidatorContract extends PartialLiquidatorContract { 17 | @Logger("AAVEPartialLiquidator") 18 | logger!: ILogger; 19 | 20 | constructor(router: Address, bot: Address) { 21 | super("AAVE Partial Liquidator", router, bot); 22 | } 23 | 24 | public async deploy(): Promise { 25 | let address = this.config.aavePartialLiquidatorAddress; 26 | const aavePool = 27 | contractsByNetwork[this.config.network].AAVE_V3_LENDING_POOL; 28 | if (!address) { 29 | this.logger.debug( 30 | { aavePool, router: this.router, bot: this.bot }, 31 | "deploying partial liquidator", 32 | ); 33 | 34 | let hash = await this.client.wallet.deployContract({ 35 | abi: aaveFlTakerAbi, 36 | bytecode: AaveFLTaker_bytecode, 37 | args: [aavePool], 38 | }); 39 | this.logger.debug(`waiting for AaveFLTaker to deploy, tx hash: ${hash}`); 40 | const { contractAddress: aaveFlTakerAddr } = 41 | await this.client.pub.waitForTransactionReceipt({ 42 | hash, 43 | timeout: 120_000, 44 | }); 45 | if (!aaveFlTakerAddr) { 46 | throw new Error(`AaveFLTaker was not deployed, tx hash: ${hash}`); 47 | } 48 | let owner = await this.client.pub.readContract({ 49 | abi: aaveFlTakerAbi, 50 | functionName: "owner", 51 | address: aaveFlTakerAddr, 52 | }); 53 | this.logger.debug( 54 | `deployed AaveFLTaker at ${aaveFlTakerAddr} owned by ${owner} in tx ${hash}`, 55 | ); 56 | 57 | hash = await this.client.wallet.deployContract({ 58 | abi: aaveLiquidatorAbi, 59 | bytecode: AaveLiquidator_bytecode, 60 | args: [this.router, this.bot, aavePool, aaveFlTakerAddr], 61 | }); 62 | this.logger.debug(`waiting for liquidator to deploy, tx hash: ${hash}`); 63 | const { contractAddress: liquidatorAddr } = 64 | await this.client.pub.waitForTransactionReceipt({ 65 | hash, 66 | timeout: 120_000, 67 | }); 68 | if (!liquidatorAddr) { 69 | throw new Error(`liquidator was not deployed, tx hash: ${hash}`); 70 | } 71 | owner = await this.client.pub.readContract({ 72 | abi: aaveLiquidatorAbi, 73 | address: liquidatorAddr, 74 | functionName: "owner", 75 | }); 76 | this.logger.debug( 77 | `deployed Liquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, 78 | ); 79 | 80 | const receipt = await this.client.simulateAndWrite({ 81 | address: aaveFlTakerAddr, 82 | abi: aaveFlTakerAbi, 83 | functionName: "setAllowedFLReceiver", 84 | args: [liquidatorAddr, true], 85 | }); 86 | if (receipt.status === "reverted") { 87 | throw new Error( 88 | `AaveFLTaker.setAllowedFLReceiver reverted, tx hash: ${receipt.transactionHash}`, 89 | ); 90 | } 91 | this.logger.debug( 92 | `set allowed flashloan receiver on FLTaker ${aaveFlTakerAddr} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, 93 | ); 94 | 95 | address = liquidatorAddr; 96 | } 97 | this.logger.info(`partial liquidator contract addesss: ${address}`); 98 | this.address = address; 99 | } 100 | 101 | public get envVariable(): [key: string, value: string] { 102 | return ["AAVE_PARTIAL_LIQUIDATOR_ADDRESS", this.address]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/services/liquidate/GHOLiquidatorContract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ghoFmTakerAbi, 3 | ghoLiquidatorAbi, 4 | } from "@gearbox-protocol/liquidator-v2-contracts/abi"; 5 | import { 6 | GhoFMTaker_bytecode, 7 | GhoLiquidator_bytecode, 8 | } from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; 9 | import { tokenDataByNetwork } from "@gearbox-protocol/sdk-gov"; 10 | import type { Address } from "viem"; 11 | 12 | import type { ILogger } from "../../log/index.js"; 13 | import { Logger } from "../../log/index.js"; 14 | import PartialLiquidatorContract from "./PartialLiquidatorContract.js"; 15 | 16 | export default class GHOLiquidatorContract extends PartialLiquidatorContract { 17 | @Logger("GHOPartialLiquidator") 18 | logger!: ILogger; 19 | #token: "DOLA" | "GHO"; 20 | 21 | constructor(router: Address, bot: Address, token: "DOLA" | "GHO") { 22 | super(`${token} Partial Liquidator`, router, bot); 23 | this.#token = token; 24 | } 25 | 26 | public async deploy(): Promise { 27 | let address = this.deployedAddress; 28 | if (!address) { 29 | this.logger.debug( 30 | { 31 | flashMinter: this.flashMinter, 32 | router: this.router, 33 | bot: this.bot, 34 | token: this.#token, 35 | }, 36 | "deploying partial liquidator", 37 | ); 38 | 39 | let hash = await this.client.wallet.deployContract({ 40 | abi: ghoFmTakerAbi, 41 | bytecode: GhoFMTaker_bytecode, 42 | // constructor(address _ghoFlashMinter, address _gho) { 43 | args: [ 44 | this.flashMinter, 45 | tokenDataByNetwork[this.config.network][this.#token], 46 | ], 47 | }); 48 | this.logger.debug(`waiting for GhoFMTaker to deploy, tx hash: ${hash}`); 49 | const { contractAddress: ghoFMTakerAddr } = 50 | await this.client.pub.waitForTransactionReceipt({ 51 | hash, 52 | timeout: 120_000, 53 | }); 54 | if (!ghoFMTakerAddr) { 55 | throw new Error(`GhoFMTaker was not deployed, tx hash: ${hash}`); 56 | } 57 | let owner = await this.client.pub.readContract({ 58 | abi: ghoFmTakerAbi, 59 | functionName: "owner", 60 | address: ghoFMTakerAddr, 61 | }); 62 | this.logger.debug( 63 | `deployed GhoFMTaker at ${ghoFMTakerAddr} owned by ${owner} in tx ${hash}`, 64 | ); 65 | 66 | hash = await this.client.wallet.deployContract({ 67 | abi: ghoLiquidatorAbi, 68 | bytecode: GhoLiquidator_bytecode, 69 | // address _router, address _plb, address _ghoFlashMinter, address _ghoFMTaker, address _gho 70 | args: [ 71 | this.router, 72 | this.bot, 73 | this.flashMinter, 74 | ghoFMTakerAddr, 75 | tokenDataByNetwork[this.config.network][this.#token], 76 | ], 77 | }); 78 | this.logger.debug(`waiting for liquidator to deploy, tx hash: ${hash}`); 79 | const { contractAddress: liquidatorAddr } = 80 | await this.client.pub.waitForTransactionReceipt({ 81 | hash, 82 | timeout: 120_000, 83 | }); 84 | if (!liquidatorAddr) { 85 | throw new Error(`liquidator was not deployed, tx hash: ${hash}`); 86 | } 87 | owner = await this.client.pub.readContract({ 88 | abi: ghoLiquidatorAbi, 89 | address: liquidatorAddr, 90 | functionName: "owner", 91 | }); 92 | this.logger.debug( 93 | `deployed Liquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, 94 | ); 95 | 96 | const receipt = await this.client.simulateAndWrite({ 97 | address: ghoFMTakerAddr, 98 | abi: ghoFmTakerAbi, 99 | functionName: "setAllowedFMReceiver", 100 | args: [liquidatorAddr, true], 101 | }); 102 | if (receipt.status === "reverted") { 103 | throw new Error( 104 | `GhoFMTaker.setAllowedFMReceiver reverted, tx hash: ${receipt.transactionHash}`, 105 | ); 106 | } 107 | this.logger.debug( 108 | `set allowed flashloan receiver on FMTaker ${ghoFMTakerAddr} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, 109 | ); 110 | 111 | address = liquidatorAddr; 112 | } 113 | this.logger.info(`partial liquidator contract addesss: ${address}`); 114 | this.address = address; 115 | } 116 | 117 | private get deployedAddress(): Address | undefined { 118 | switch (this.#token) { 119 | case "GHO": 120 | return this.config.ghoPartialLiquidatorAddress; 121 | case "DOLA": 122 | return this.config.dolaPartialLiquidatorAddress; 123 | } 124 | return undefined; 125 | } 126 | 127 | private get flashMinter(): Address { 128 | if (this.config.network === "Mainnet") { 129 | switch (this.#token) { 130 | case "GHO": 131 | return "0xb639D208Bcf0589D54FaC24E655C79EC529762B8"; 132 | case "DOLA": 133 | return "0x6C5Fdc0c53b122Ae0f15a863C349f3A481DE8f1F"; 134 | } 135 | } 136 | throw new Error( 137 | `${this.#token} flash minter is not available on ${this.config.network}`, 138 | ); 139 | } 140 | 141 | public get envVariable(): [key: string, value: string] { 142 | return [`${this.#token}_PARTIAL_LIQUIDATOR_ADDRESS`, this.address]; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/services/liquidate/OptimisiticResults.ts: -------------------------------------------------------------------------------- 1 | import type { OptimisticResult } from "@gearbox-protocol/types/optimist"; 2 | 3 | import { DI } from "../../di.js"; 4 | 5 | @DI.Injectable(DI.OptimisticResults) 6 | export class OptimisticResults { 7 | #results: OptimisticResult[] = []; 8 | 9 | public push(result: OptimisticResult): void { 10 | this.#results.push(result); 11 | } 12 | 13 | public get(): OptimisticResult[] { 14 | return this.#results; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/liquidate/SiloLiquidatorContract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | siloFlTakerAbi, 3 | siloLiquidatorAbi, 4 | } from "@gearbox-protocol/liquidator-v2-contracts/abi"; 5 | import { 6 | SiloFLTaker_bytecode, 7 | SiloLiquidator_bytecode, 8 | } from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; 9 | import { tokenDataByNetwork } from "@gearbox-protocol/sdk-gov"; 10 | import type { Address } from "viem"; 11 | 12 | import type { ILogger } from "../../log/index.js"; 13 | import { Logger } from "../../log/index.js"; 14 | import PartialLiquidatorContract from "./PartialLiquidatorContract.js"; 15 | 16 | const SONIC_USDCE_SILO: Address = "0x322e1d5384aa4ED66AeCa770B95686271de61dc3"; 17 | const SONIC_WS_SILO: Address = "0xf55902DE87Bd80c6a35614b48d7f8B612a083C12"; 18 | 19 | export default class SiloLiquidatorContract extends PartialLiquidatorContract { 20 | @Logger("SiloPartialLiquidator") 21 | logger!: ILogger; 22 | 23 | #siloFLTaker: Address | undefined; 24 | 25 | constructor(router: Address, bot: Address) { 26 | super("Silo Partial Liquidator", router, bot); 27 | } 28 | 29 | public async deploy(): Promise { 30 | let address = this.config.siloPartialLiquidatorAddress; 31 | if (!address) { 32 | this.logger.debug( 33 | { 34 | router: this.router, 35 | bot: this.bot, 36 | }, 37 | "deploying partial liquidator", 38 | ); 39 | 40 | let hash = await this.client.wallet.deployContract({ 41 | abi: siloFlTakerAbi, 42 | bytecode: SiloFLTaker_bytecode, 43 | }); 44 | this.logger.debug(`waiting for SiloFLTaker to deploy, tx hash: ${hash}`); 45 | const { contractAddress: siloFLTakerAddr } = 46 | await this.client.pub.waitForTransactionReceipt({ 47 | hash, 48 | timeout: 120_000, 49 | }); 50 | if (!siloFLTakerAddr) { 51 | throw new Error(`SiloFLTaker was not deployed, tx hash: ${hash}`); 52 | } 53 | this.#siloFLTaker = siloFLTakerAddr; 54 | let owner = await this.client.pub.readContract({ 55 | abi: siloFlTakerAbi, 56 | functionName: "owner", 57 | address: this.siloFLTaker, 58 | }); 59 | this.logger.debug( 60 | `deployed SiloFLTaker at ${this.siloFLTaker} owned by ${owner} in tx ${hash}`, 61 | ); 62 | 63 | hash = await this.client.wallet.deployContract({ 64 | abi: siloLiquidatorAbi, 65 | bytecode: SiloLiquidator_bytecode, 66 | // constructor(address _router, address _plb, address _siloFLTaker) AbstractLiquidator(_router, _plb) { 67 | args: [this.router, this.bot, this.siloFLTaker], 68 | }); 69 | this.logger.debug( 70 | `waiting for SiloLiquidator to deploy, tx hash: ${hash}`, 71 | ); 72 | const { contractAddress: liquidatorAddr } = 73 | await this.client.pub.waitForTransactionReceipt({ 74 | hash, 75 | timeout: 120_000, 76 | }); 77 | if (!liquidatorAddr) { 78 | throw new Error(`SiloLiquidator was not deployed, tx hash: ${hash}`); 79 | } 80 | owner = await this.client.pub.readContract({ 81 | abi: siloLiquidatorAbi, 82 | address: liquidatorAddr, 83 | functionName: "owner", 84 | }); 85 | this.logger.debug( 86 | `deployed SiloLiquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, 87 | ); 88 | 89 | // siloFLTaker.setTokenToSilo(tokenTestSuite.addressOf(TOKEN_USDC_e), SONIC_USDCE_SILO); 90 | // siloFLTaker.setTokenToSilo(tokenTestSuite.addressOf(TOKEN_wS), SONIC_WS_SILO); 91 | 92 | // siloFLTaker.setAllowedFLReceiver(address(liquidator), true); 93 | const receipt = await this.client.simulateAndWrite({ 94 | address: this.siloFLTaker, 95 | abi: siloFlTakerAbi, 96 | functionName: "setAllowedFLReceiver", 97 | args: [liquidatorAddr, true], 98 | }); 99 | if (receipt.status === "reverted") { 100 | throw new Error( 101 | `SiloFLTaker.setAllowedFLReceiver reverted, tx hash: ${receipt.transactionHash}`, 102 | ); 103 | } 104 | this.logger.debug( 105 | `set allowed flashloan receiver on SiloFLTaker ${this.siloFLTaker} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, 106 | ); 107 | 108 | await this.setTokenToSilo( 109 | tokenDataByNetwork.Sonic.USDC_e, 110 | SONIC_USDCE_SILO, 111 | ); 112 | await this.setTokenToSilo(tokenDataByNetwork.Sonic.wS, SONIC_WS_SILO); 113 | 114 | address = liquidatorAddr; 115 | } 116 | this.logger.info(`partial liquidator contract addesss: ${address}`); 117 | this.address = address; 118 | } 119 | 120 | public async setTokenToSilo(token: Address, silo: Address): Promise { 121 | const receipt = await this.client.simulateAndWrite({ 122 | address: this.siloFLTaker, 123 | abi: siloFlTakerAbi, 124 | functionName: "setTokenToSilo", 125 | args: [token, silo], 126 | }); 127 | if (receipt.status === "reverted") { 128 | throw new Error( 129 | `SiloFLTaker.setTokenToSilo(${token}, ${silo}) reverted, tx hash: ${receipt.transactionHash}`, 130 | ); 131 | } 132 | this.logger.debug( 133 | `set token ${token} to silo ${silo} on SiloFLTaker ${this.siloFLTaker} in tx ${receipt.transactionHash}`, 134 | ); 135 | } 136 | 137 | private get siloFLTaker(): Address { 138 | if (!this.#siloFLTaker) { 139 | throw new Error("SiloFLTaker is not deployed"); 140 | } 141 | return this.#siloFLTaker; 142 | } 143 | 144 | public get envVariable(): [key: string, value: string] { 145 | return ["SILO_PARTIAL_LIQUIDATOR_ADDRESS", this.address]; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/services/liquidate/SingularFullLiquidator.ts: -------------------------------------------------------------------------------- 1 | import { iCreditFacadeV3Abi } from "@gearbox-protocol/types/abi"; 2 | import type { SimulateContractReturnType } from "viem"; 3 | 4 | import { type CreditAccountData, exceptionsAbis } from "../../data/index.js"; 5 | import SingularLiquidator from "./SingularLiquidator.js"; 6 | import type { 7 | FullLiquidationPreview, 8 | MakeLiquidatableResult, 9 | } from "./types.js"; 10 | 11 | export default class SingularFullLiquidator extends SingularLiquidator { 12 | protected readonly name = "full"; 13 | protected readonly adverb = "fully"; 14 | 15 | public async makeLiquidatable( 16 | ca: CreditAccountData, 17 | ): Promise { 18 | // not supported 19 | return Promise.resolve({}); 20 | } 21 | 22 | public async preview(ca: CreditAccountData): Promise { 23 | try { 24 | const cm = await this.getCreditManagerData(ca.creditManager); 25 | 26 | const result = await this.pathFinder.findBestClosePath( 27 | ca, 28 | cm, 29 | this.config.slippage, 30 | ); 31 | if (!result) { 32 | throw new Error("pathfinder result is empty"); 33 | } 34 | // we want fresh redstone price in actual liquidation transactions 35 | const priceUpdates = await this.redstone.liquidationPreviewUpdates( 36 | ca, 37 | true, 38 | ); 39 | return { 40 | amount: result.amount, 41 | minAmount: result.minAmount, 42 | underlyingBalance: result.underlyingBalance, 43 | calls: [ 44 | ...this.redstone.toMulticallUpdates(ca, priceUpdates), 45 | ...result.calls, 46 | ], 47 | priceUpdates, 48 | }; 49 | } catch (e) { 50 | throw new Error(`cant find close path: ${e}`); 51 | } 52 | } 53 | 54 | public async simulate( 55 | account: CreditAccountData, 56 | preview: FullLiquidationPreview, 57 | ): Promise { 58 | return this.client.pub.simulateContract({ 59 | account: this.client.account, 60 | abi: [...iCreditFacadeV3Abi, ...exceptionsAbis], 61 | address: account.creditFacade, 62 | functionName: "liquidateCreditAccount", 63 | args: [account.addr, this.client.address, preview.calls], 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/services/liquidate/SingularLiquidator.ts: -------------------------------------------------------------------------------- 1 | import type { OptimisticResult } from "@gearbox-protocol/types/optimist"; 2 | import type { Hex, SimulateContractReturnType } from "viem"; 3 | 4 | import type { CreditAccountData } from "../../data/index.js"; 5 | import { TxParserHelper } from "../../utils/ethers-6-temp/txparser/index.js"; 6 | import { 7 | LiquidationErrorMessage, 8 | LiquidationStartMessage, 9 | LiquidationSuccessMessage, 10 | } from "../notifier/index.js"; 11 | import AbstractLiquidator from "./AbstractLiquidator.js"; 12 | import type { 13 | ILiquidatorService, 14 | MakeLiquidatableResult, 15 | StrategyPreview, 16 | } from "./types.js"; 17 | 18 | export default abstract class SingularLiquidator 19 | extends AbstractLiquidator 20 | implements ILiquidatorService 21 | { 22 | protected abstract readonly name: string; 23 | protected abstract readonly adverb: string; 24 | 25 | public async liquidate(accounts: CreditAccountData[]): Promise { 26 | if (!accounts.length) { 27 | return; 28 | } 29 | this.logger.warn(`Need to liquidate ${accounts.length} accounts`); 30 | for (const ca of accounts) { 31 | await this.#liquidateOne(ca); 32 | } 33 | } 34 | 35 | public async liquidateOptimistic( 36 | accounts: CreditAccountData[], 37 | ): Promise { 38 | const total = accounts.length; 39 | const debugS = this.config.debugAccounts ? "selective " : " "; 40 | this.logger.info(`${debugS}optimistic liquidation for ${total} accounts`); 41 | 42 | for (let i = 0; i < total; i++) { 43 | const acc = accounts[i]; 44 | const result = await this.#liquidateOneOptimistic(acc); 45 | const status = result.isError ? "FAIL" : "OK"; 46 | const msg = `[${i + 1}/${total}] ${acc.addr} in ${acc.creditManager} ${status}`; 47 | if (result.isError) { 48 | this.logger.warn(msg); 49 | } else { 50 | this.logger.info(msg); 51 | } 52 | } 53 | const success = this.optimistic.get().filter(r => !r.isError).length; 54 | this.logger.info( 55 | `optimistic liquidation finished: ${success}/${total} accounts liquidated`, 56 | ); 57 | } 58 | 59 | async #liquidateOne(ca: CreditAccountData): Promise { 60 | const logger = this.logger.child({ 61 | account: ca.addr, 62 | borrower: ca.borrower, 63 | manager: ca.managerName, 64 | }); 65 | if (this.skipList.has(ca.addr)) { 66 | this.logger.warn("skipping this account"); 67 | return; 68 | } 69 | logger.info(`begin ${this.name} liquidation: HF = ${ca.healthFactor}`); 70 | this.notifier.notify(new LiquidationStartMessage(ca, this.name)); 71 | let pathHuman: string[] | undefined; 72 | let preview: T | undefined; 73 | try { 74 | preview = await this.preview(ca); 75 | pathHuman = TxParserHelper.parseMultiCall(preview); 76 | logger.debug({ pathHuman }, "path found"); 77 | 78 | const { request } = await this.simulate(ca, preview); 79 | const receipt = await this.client.liquidate(request, logger); 80 | 81 | this.notifier.alert( 82 | new LiquidationSuccessMessage(ca, this.adverb, receipt, pathHuman), 83 | ); 84 | } catch (e) { 85 | const decoded = await this.errorHandler.explain(e, ca); 86 | logger.error(decoded, "cant liquidate"); 87 | if (preview?.skipOnFailure) { 88 | this.skipList.add(ca.addr); 89 | this.logger.warn("adding to skip list"); 90 | } 91 | // mechanism to be less annoyed with telegram spam 92 | const severity = this.getAlertBucket(ca).chooseSeverity(); 93 | this.notifier[severity]( 94 | new LiquidationErrorMessage( 95 | ca, 96 | this.adverb, 97 | decoded.shortMessage, 98 | pathHuman, 99 | preview?.skipOnFailure, 100 | ), 101 | ); 102 | } 103 | } 104 | 105 | async #liquidateOneOptimistic( 106 | acc: CreditAccountData, 107 | ): Promise { 108 | const logger = this.logger.child({ 109 | account: acc.addr, 110 | borrower: acc.borrower, 111 | manager: acc.managerName, 112 | }); 113 | let snapshotId: Hex | undefined; 114 | let result = this.newOptimisticResult(acc); 115 | const start = Date.now(); 116 | try { 117 | const balanceBefore = await this.getExecutorBalance(acc.underlyingToken); 118 | const mlRes = await this.makeLiquidatable(acc); 119 | snapshotId = mlRes.snapshotId; 120 | result.partialLiquidationCondition = mlRes.partialLiquidationCondition; 121 | logger.debug({ snapshotId }, "previewing..."); 122 | const preview = await this.preview(acc); 123 | logger.debug({ pathHuman: result.callsHuman }, "path found"); 124 | result = this.updateAfterPreview(result, preview); 125 | 126 | const { request } = await this.simulate(acc, preview); 127 | 128 | // snapshotId might be present if we had to setup liquidation conditions for single account 129 | // otherwise, not write requests has been made up to this point, and it's safe to take snapshot now 130 | if (!snapshotId) { 131 | snapshotId = await this.client.anvil.snapshot(); 132 | } 133 | // ------ Actual liquidation (write request start here) ----- 134 | const receipt = await this.client.liquidate(request, logger); 135 | logger.debug(`Liquidation tx hash: ${receipt.transactionHash}`); 136 | result.isError = receipt.status === "reverted"; 137 | logger.debug( 138 | `Liquidation tx receipt: status=${receipt.status}, gas=${receipt.cumulativeGasUsed.toString()}`, 139 | ); 140 | // ------ End of actual liquidation 141 | result = await this.updateAfterLiquidation( 142 | result, 143 | acc, 144 | balanceBefore.underlying, 145 | receipt, 146 | ); 147 | // swap underlying back to ETH 148 | await this.swapper.swap( 149 | acc.underlyingToken, 150 | balanceBefore.underlying + BigInt(result.liquidatorPremium), 151 | ); 152 | const balanceAfter = await this.getExecutorBalance(acc.underlyingToken); 153 | result.liquidatorProfit = (balanceAfter.eth - balanceBefore.eth).toString( 154 | 10, 155 | ); 156 | } catch (e: any) { 157 | const decoded = await this.errorHandler.explain(e, acc, true); 158 | result.traceFile = decoded.traceFile; 159 | result.error = `cannot liquidate: ${decoded.longMessage}`.replaceAll( 160 | "\n", 161 | "\\n", 162 | ); 163 | logger.error({ decoded }, "cannot liquidate"); 164 | } 165 | 166 | result.duration = Date.now() - start; 167 | this.optimistic.push(result); 168 | 169 | if (snapshotId) { 170 | await this.client.anvil.revert({ id: snapshotId }); 171 | } 172 | 173 | return result; 174 | } 175 | 176 | /** 177 | * For optimistic liquidations only: create conditions that make this account liquidatable 178 | * If strategy implements this scenario, it must make evm_snapshot beforehand and return it as a result 179 | * Id strategy does not support this, return undefined 180 | * @param ca 181 | * @returns evm snapshotId or underfined 182 | */ 183 | abstract makeLiquidatable( 184 | ca: CreditAccountData, 185 | ): Promise; 186 | 187 | abstract preview(ca: CreditAccountData): Promise; 188 | /** 189 | * Simulates liquidation 190 | * @param account 191 | * @param preview 192 | * @returns 193 | */ 194 | abstract simulate( 195 | account: CreditAccountData, 196 | preview: T, 197 | ): Promise; 198 | } 199 | -------------------------------------------------------------------------------- /src/services/liquidate/factory.ts: -------------------------------------------------------------------------------- 1 | import type { IFactory } from "di-at-home"; 2 | 3 | import type { Config } from "../../config/index.js"; 4 | import { DI } from "../../di.js"; 5 | import BatchLiquidator from "./BatchLiquidator.js"; 6 | import SingularFullLiquidator from "./SingularFullLiquidator.js"; 7 | import SingularPartialLiquidator from "./SingularPartialLiquidator.js"; 8 | import type { ILiquidatorService } from "./types.js"; 9 | 10 | @DI.Factory(DI.Liquidator) 11 | export class LiquidatorFactory implements IFactory { 12 | @DI.Inject(DI.Config) 13 | config!: Config; 14 | 15 | produce(): ILiquidatorService { 16 | if (this.config.isPartial) { 17 | return new SingularPartialLiquidator(); 18 | } 19 | if (this.config.isBatch) { 20 | return new BatchLiquidator(); 21 | } 22 | return new SingularFullLiquidator(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/liquidate/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./factory.js"; 2 | export * from "./OptimisiticResults.js"; 3 | export type * from "./types.js"; 4 | export type * from "./viem-types.js"; 5 | -------------------------------------------------------------------------------- /src/services/liquidate/types.ts: -------------------------------------------------------------------------------- 1 | import type { PartialLiquidationCondition } from "@gearbox-protocol/types/optimist"; 2 | import type { Address, Hash, Hex } from "viem"; 3 | 4 | import type { 5 | CreditAccountData, 6 | MultiCall, 7 | PriceOnDemand, 8 | } from "../../data/index.js"; 9 | import type { PathFinderCloseResult } from "../../utils/ethers-6-temp/pathfinder/index.js"; 10 | 11 | export interface PriceOnDemandExtras extends PriceOnDemand { 12 | /** 13 | * Price feed address 14 | */ 15 | address: Address; 16 | dataFeedId: string; 17 | /** 18 | * In case when token in PriceOnDemand is ticker, this will be the original token 19 | * Otherwise they are the same 20 | */ 21 | originalToken: Address; 22 | ts: number; 23 | reserve: boolean; 24 | } 25 | 26 | export interface PriceUpdate { 27 | token: Address; 28 | data: `0x${string}`; 29 | reserve: boolean; 30 | } 31 | 32 | export interface FullLiquidationPreview extends PathFinderCloseResult { 33 | priceUpdates: PriceUpdate[]; 34 | } 35 | 36 | export interface PartialLiquidationPreview { 37 | calls: MultiCall[]; 38 | assetOut: Address; 39 | amountOut: bigint; 40 | flashLoanAmount: bigint; 41 | underlyingBalance: bigint; 42 | priceUpdates: PriceUpdate[]; 43 | skipOnFailure?: boolean; 44 | } 45 | 46 | export type PartialLiquidationPreviewWithFallback = 47 | | (PartialLiquidationPreview & { 48 | fallback: false; 49 | }) 50 | | (FullLiquidationPreview & { fallback: true }); 51 | 52 | export interface ILiquidatorService { 53 | launch: (asFallback?: boolean) => Promise; 54 | liquidate: (accounts: CreditAccountData[]) => Promise; 55 | /** 56 | * 57 | * @param ca 58 | * @param redstoneTokens 59 | * @returns true is account was successfully liquidated 60 | */ 61 | liquidateOptimistic: (accounts: CreditAccountData[]) => Promise; 62 | } 63 | 64 | export interface StrategyPreview { 65 | calls: MultiCall[]; 66 | underlyingBalance: bigint; 67 | /** 68 | * Asset in case of partial liquidation 69 | */ 70 | assetOut?: Address; 71 | /** 72 | * Asset amount in case of partial liquidation 73 | */ 74 | amountOut?: bigint; 75 | /** 76 | * Falsh loan amount in case of partial liquidation 77 | */ 78 | flashLoanAmount?: bigint; 79 | priceUpdates?: PriceUpdate[]; 80 | /** 81 | * If true, will not attempt to liquidate this account again 82 | */ 83 | skipOnFailure?: boolean; 84 | } 85 | 86 | export interface MakeLiquidatableResult { 87 | snapshotId?: Hex; 88 | partialLiquidationCondition?: PartialLiquidationCondition; 89 | } 90 | 91 | export interface MerkleDistributorInfo { 92 | merkleRoot: Hash; 93 | tokenTotal: string; 94 | claims: Record< 95 | Address, 96 | { 97 | index: number; 98 | amount: string; 99 | proof: Hash[]; 100 | } 101 | >; 102 | } 103 | -------------------------------------------------------------------------------- /src/services/liquidate/viem-types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | iBatchLiquidatorAbi, 3 | iPartialLiquidatorAbi, 4 | iPriceHelperAbi, 5 | } from "@gearbox-protocol/liquidator-v2-contracts/abi"; 6 | import type { AbiParameterToPrimitiveType, ExtractAbiFunction } from "abitype"; 7 | import type { GetContractReturnType, PublicClient } from "viem"; 8 | 9 | import type { ArrayElementType } from "../../utils/index.js"; 10 | 11 | export type IPriceHelperContract = GetContractReturnType< 12 | typeof iPriceHelperAbi, 13 | PublicClient 14 | >; 15 | 16 | export type IPartialLiquidatorContract = GetContractReturnType< 17 | typeof iPartialLiquidatorAbi, 18 | PublicClient 19 | >; 20 | 21 | export type TokenPriceInfo = ArrayElementType< 22 | AbiParameterToPrimitiveType< 23 | ExtractAbiFunction["outputs"]["0"] 24 | > 25 | >; 26 | 27 | export type BatchLiquidationResult = ArrayElementType< 28 | AbiParameterToPrimitiveType< 29 | ExtractAbiFunction< 30 | typeof iBatchLiquidatorAbi, 31 | "estimateBatch" 32 | >["outputs"]["0"] 33 | > 34 | >; 35 | 36 | export type LiquidateBatchInput = ArrayElementType< 37 | AbiParameterToPrimitiveType< 38 | ExtractAbiFunction< 39 | typeof iBatchLiquidatorAbi, 40 | "liquidateBatch" 41 | >["inputs"]["0"] 42 | > 43 | >; 44 | -------------------------------------------------------------------------------- /src/services/notifier/AlertBucket.ts: -------------------------------------------------------------------------------- 1 | import type { INotifier } from "./types.js"; 2 | 3 | export class AlertBucket { 4 | #intervals; 5 | #defaultInterval; 6 | #nextAlert; 7 | 8 | constructor(intervals: number[], defaultInterval = 60 * 60 * 1000) { 9 | this.#nextAlert = Date.now(); 10 | this.#intervals = intervals; 11 | this.#defaultInterval = defaultInterval; 12 | } 13 | 14 | public chooseSeverity(): keyof INotifier { 15 | const now = Date.now(); 16 | if (now >= this.#nextAlert) { 17 | let nextIncrement = this.#defaultInterval; 18 | if (this.#intervals.length > 0) { 19 | nextIncrement = this.#intervals.shift()!; 20 | } 21 | this.#nextAlert = now + nextIncrement; 22 | return "alert"; 23 | } 24 | return "notify"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/services/notifier/ConsoleNotifier.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from "../../log/index.js"; 2 | import { Logger } from "../../log/index.js"; 3 | import type { INotifier, INotifierMessage } from "./types.js"; 4 | 5 | export default class ConsoleNotifier implements INotifier { 6 | @Logger("ConsoleNotifier") 7 | log!: ILogger; 8 | 9 | public alert(message: INotifierMessage): void { 10 | this.log.warn(message.plain); 11 | } 12 | 13 | public notify(message: INotifierMessage): void { 14 | this.log.info(message.plain); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/notifier/TelegramNotifier.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from "axios"; 2 | import axios, { isAxiosError } from "axios"; 3 | import axiosRetry, { 4 | exponentialDelay, 5 | isNetworkError, 6 | isRetryableError, 7 | } from "axios-retry"; 8 | 9 | import type { Config } from "../../config/index.js"; 10 | import { DI } from "../../di.js"; 11 | import type { ILogger } from "../../log/index.js"; 12 | import { Logger } from "../../log/index.js"; 13 | import type { INotifier, INotifierMessage } from "./types.js"; 14 | 15 | export default class TelegramNotifier implements INotifier { 16 | @Logger("TelegramNotifier") 17 | log!: ILogger; 18 | 19 | @DI.Inject(DI.Config) 20 | config!: Config; 21 | 22 | #messageOptions: Record = { 23 | parse_mode: "MarkdownV2", 24 | link_preview_options: { is_disabled: true }, 25 | }; 26 | #client?: AxiosInstance; 27 | 28 | public alert(message: INotifierMessage): void { 29 | this.#sendToTelegram( 30 | message.markdown, 31 | this.config.telegramAlersChannel!, 32 | "alert", 33 | ).catch(console.error); 34 | } 35 | 36 | public notify(message: INotifierMessage): void { 37 | this.#sendToTelegram( 38 | message.markdown, 39 | this.config.telegramNotificationsChannel!, 40 | ).catch(console.error); 41 | } 42 | 43 | async #sendToTelegram( 44 | text: string, 45 | channelId: string, 46 | severity = "notification", 47 | ): Promise { 48 | this.log.debug(`sending telegram ${severity} to channel ${channelId}...`); 49 | try { 50 | await this.client.post("", { 51 | ...this.#messageOptions, 52 | chat_id: channelId, 53 | text, 54 | }); 55 | this.log.info(`telegram ${severity} sent successfully`); 56 | } catch (e) { 57 | if (isAxiosError(e)) { 58 | this.log.error( 59 | { 60 | status: e.response?.status, 61 | data: e.response?.data, 62 | code: e.code, 63 | }, 64 | `cannot send telegram ${severity}: ${e.message}`, 65 | ); 66 | } else { 67 | this.log.error(`cannot send telegram ${severity}: ${e}`); 68 | } 69 | } 70 | } 71 | 72 | private get client(): AxiosInstance { 73 | if (!this.#client) { 74 | this.#client = axios.create({ 75 | baseURL: `https://api.telegram.org/bot${this.config.telegramBotToken!}/sendMessage`, 76 | headers: { 77 | "Content-Type": "application/json", 78 | }, 79 | }); 80 | axiosRetry(this.#client, { 81 | retries: 10, 82 | retryDelay: exponentialDelay, 83 | retryCondition: e => { 84 | return ( 85 | isNetworkError(e) || 86 | isRetryableError(e) || 87 | (e.response?.data as any)?.error_code === 429 88 | ); 89 | }, 90 | }); 91 | } 92 | return this.#client; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/services/notifier/factory.ts: -------------------------------------------------------------------------------- 1 | import type { IFactory } from "di-at-home"; 2 | 3 | import type { Config } from "../../config/index.js"; 4 | import { DI } from "../../di.js"; 5 | import ConsoleNotifier from "./ConsoleNotifier.js"; 6 | import TelegramNotifier from "./TelegramNotifier.js"; 7 | import type { INotifier } from "./types.js"; 8 | 9 | @DI.Factory(DI.Notifier) 10 | export class NotifierFactory implements IFactory { 11 | @DI.Inject(DI.Config) 12 | config!: Config; 13 | 14 | produce(): INotifier { 15 | if ( 16 | this.config.telegramBotToken && 17 | this.config.telegramAlersChannel && 18 | this.config.telegramNotificationsChannel 19 | ) { 20 | return new TelegramNotifier(); 21 | } 22 | return new ConsoleNotifier(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/notifier/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AlertBucket.js"; 2 | export * from "./factory.js"; 3 | export * from "./messages.js"; 4 | export * from "./types.js"; 5 | -------------------------------------------------------------------------------- /src/services/notifier/types.ts: -------------------------------------------------------------------------------- 1 | export interface INotifierMessage { 2 | markdown: string; 3 | plain: string; 4 | } 5 | 6 | export interface INotifier { 7 | alert: (message: INotifierMessage) => void; 8 | notify: (message: INotifierMessage) => void; 9 | } 10 | -------------------------------------------------------------------------------- /src/services/output/BaseWriter.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "../../config/index.js"; 2 | import { DI } from "../../di.js"; 3 | import type { ILogger } from "../../log/index.js"; 4 | import { Logger } from "../../log/index.js"; 5 | import { json_stringify } from "../../utils/bigint-serializer.js"; 6 | import type { OptimisticResults } from "../liquidate/index.js"; 7 | 8 | export default class BaseWriter { 9 | @Logger("OutputWriter") 10 | log!: ILogger; 11 | 12 | @DI.Inject(DI.Config) 13 | config!: Config; 14 | 15 | @DI.Inject(DI.OptimisticResults) 16 | optimistic!: OptimisticResults; 17 | 18 | protected get filename(): string { 19 | let fname = this.config.outFileName; 20 | if (!fname) { 21 | throw new Error(`out file name not specified in config`); 22 | } 23 | return fname.endsWith(".json") ? fname : `${fname}.json`; 24 | } 25 | 26 | protected get content(): string { 27 | return json_stringify({ 28 | result: this.optimistic.get(), 29 | startBlock: this.config.startBlock, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/services/output/consoleWriter.ts: -------------------------------------------------------------------------------- 1 | import BaseWriter from "./BaseWriter.js"; 2 | import type { IOptimisticOutputWriter } from "./types.js"; 3 | 4 | export default class ConsoleWriter 5 | extends BaseWriter 6 | implements IOptimisticOutputWriter 7 | { 8 | public async write(): Promise { 9 | console.info(this.content); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/services/output/factory.ts: -------------------------------------------------------------------------------- 1 | import type { IFactory } from "di-at-home"; 2 | 3 | import type { Config } from "../../config/index.js"; 4 | import { DI } from "../../di.js"; 5 | import ConsoleWriter from "./consoleWriter.js"; 6 | import FileWriter from "./fileWriter.js"; 7 | import RestWriter from "./restWriter.js"; 8 | import S3Writer from "./s3Writer.js"; 9 | import type { IOptimisticOutputWriter } from "./types.js"; 10 | 11 | @DI.Factory(DI.Output) 12 | export class OutputWriterFactory 13 | implements IFactory 14 | { 15 | @DI.Inject(DI.Config) 16 | config!: Config; 17 | 18 | produce(): IOptimisticOutputWriter { 19 | if (this.config.outS3Bucket && this.config.outFileName) { 20 | return new S3Writer(); 21 | } else if (this.config.outEndpoint) { 22 | return new RestWriter(); 23 | } else if (this.config.outDir && this.config.outFileName) { 24 | return new FileWriter(); 25 | } 26 | return new ConsoleWriter(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/services/output/fileWriter.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | 4 | import BaseWriter from "./BaseWriter.js"; 5 | import type { IOptimisticOutputWriter } from "./types.js"; 6 | 7 | export default class FileWriter 8 | extends BaseWriter 9 | implements IOptimisticOutputWriter 10 | { 11 | public async write(): Promise { 12 | const filename = join(this.config.outDir, this.filename); 13 | try { 14 | this.log.debug(`writing to ${filename}`); 15 | await writeFile(filename, this.content, "utf-8"); 16 | } catch (e) { 17 | this.log.error(e, `failed to write to ${filename}`); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/output/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./factory.js"; 2 | export * from "./types.js"; 3 | -------------------------------------------------------------------------------- /src/services/output/restWriter.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import BaseWriter from "./BaseWriter.js"; 4 | import type { IOptimisticOutputWriter } from "./types.js"; 5 | 6 | export default class RestWriter 7 | extends BaseWriter 8 | implements IOptimisticOutputWriter 9 | { 10 | public async write(): Promise { 11 | if (!this.config.outEndpoint) { 12 | throw new Error("rest endpoint is not set"); 13 | } 14 | await axios.post(this.config.outEndpoint, this.content, { 15 | headers: { 16 | ...JSON.parse(this.config.outHeaders), 17 | "content-type": "application/json", 18 | }, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/output/s3Writer.ts: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; 4 | 5 | import BaseWriter from "./BaseWriter.js"; 6 | import type { IOptimisticOutputWriter } from "./types.js"; 7 | 8 | export default class S3Writer 9 | extends BaseWriter 10 | implements IOptimisticOutputWriter 11 | { 12 | public async write(): Promise { 13 | const key = join(this.config.outS3Prefix, this.filename); 14 | const client = new S3Client({}); 15 | try { 16 | this.log.debug(`uploading to s3://${this.config.outS3Bucket}/${key}`); 17 | await client.send( 18 | new PutObjectCommand({ 19 | Bucket: this.config.outS3Bucket, 20 | Key: key, 21 | ContentType: "application/json", 22 | Body: this.content, 23 | }), 24 | ); 25 | } catch (e) { 26 | this.log.error( 27 | e, 28 | `failed to upload to s3://${this.config.outS3Bucket}/${key}`, 29 | ); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/services/output/types.ts: -------------------------------------------------------------------------------- 1 | export interface IOptimisticOutputWriter { 2 | write: () => Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/services/scanner/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Scanner.js"; 2 | -------------------------------------------------------------------------------- /src/services/swap/base.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { tokenDataByNetwork } from "@gearbox-protocol/sdk-gov"; 3 | import type { Address } from "viem"; 4 | 5 | import type { Config } from "../../config/index.js"; 6 | import { DI } from "../../di.js"; 7 | import type Client from "../Client.js"; 8 | 9 | export default abstract class BaseSwapper { 10 | @DI.Inject(DI.Config) 11 | config!: Config; 12 | 13 | @DI.Inject(DI.Client) 14 | client!: Client; 15 | 16 | #network?: NetworkType; 17 | #wethAddr?: Address; 18 | 19 | protected async launch(network: NetworkType): Promise { 20 | this.#network = network; 21 | this.#wethAddr = tokenDataByNetwork[network].WETH; 22 | } 23 | 24 | protected get network(): NetworkType { 25 | if (!this.#network) { 26 | throw new Error("network not initialized"); 27 | } 28 | return this.#network; 29 | } 30 | 31 | protected get wethAddr(): Address { 32 | if (!this.#wethAddr) { 33 | throw new Error("weth address not initialized"); 34 | } 35 | return this.#wethAddr; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/services/swap/factory.ts: -------------------------------------------------------------------------------- 1 | import type { IFactory } from "di-at-home"; 2 | 3 | import type { Config } from "../../config/index.js"; 4 | import { DI } from "../../di.js"; 5 | import NoopSwapper from "./noop.js"; 6 | import OneInch from "./oneInch.js"; 7 | import type { ISwapper } from "./types.js"; 8 | import Uniswap from "./uniswap.js"; 9 | 10 | @DI.Factory(DI.Swapper) 11 | export class SwapperFactory implements IFactory { 12 | @DI.Inject(DI.Config) 13 | config!: Config; 14 | 15 | produce(): ISwapper { 16 | switch (this.config.swapToEth) { 17 | case "uniswap": 18 | return new Uniswap(); 19 | case "1inch": 20 | return new OneInch(); 21 | case undefined: 22 | return new NoopSwapper(); 23 | default: 24 | throw new Error(`unknown swapper ${this.config.swapToEth}`); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/services/swap/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./factory.js"; 2 | export * from "./types.js"; 3 | -------------------------------------------------------------------------------- /src/services/swap/noop.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import type { Address } from "viem"; 3 | 4 | import type { ISwapper } from "./types.js"; 5 | 6 | export default class NoopSwapper implements ISwapper { 7 | public async launch(_network: NetworkType): Promise { 8 | // nothing to do here 9 | } 10 | 11 | public async swap(_tokenAddr: Address, _amount: bigint): Promise { 12 | // nothing to do here 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/services/swap/oneInch.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { 3 | CHAINS, 4 | formatBN, 5 | getDecimals, 6 | tokenSymbolByAddress, 7 | } from "@gearbox-protocol/sdk-gov"; 8 | import { ierc20MetadataAbi } from "@gearbox-protocol/types/abi"; 9 | import type { AxiosInstance } from "axios"; 10 | import axios from "axios"; 11 | import axiosRetry from "axios-retry"; 12 | import type { Address } from "viem"; 13 | 14 | import type { ILogger } from "../../log/index.js"; 15 | import { Logger } from "../../log/index.js"; 16 | import BaseSwapper from "./base.js"; 17 | import type { ISwapper } from "./types.js"; 18 | 19 | const ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; 20 | 21 | class OneInchError extends Error { 22 | transactionHash?: string; 23 | } 24 | 25 | export default class OneInch extends BaseSwapper implements ISwapper { 26 | @Logger("one_inch") 27 | log!: ILogger; 28 | 29 | private apiClient!: AxiosInstance; 30 | private readonly slippage: number; 31 | private routerAddress: Address = "0x111111125421cA6dc452d289314280a0f8842A65"; 32 | 33 | constructor(slippage = 2) { 34 | super(); 35 | this.slippage = slippage; 36 | } 37 | 38 | public async launch(network: NetworkType): Promise { 39 | await super.launch(network); 40 | if (!this.config.oneInchApiKey) { 41 | throw new Error("1inch API key not provided"); 42 | } 43 | const baseURL = `https://api.1inch.dev/swap/v6.0/${CHAINS[network]}`; 44 | this.apiClient = axios.create({ 45 | baseURL, 46 | headers: { 47 | Authorization: `Bearer ${this.config.oneInchApiKey}`, 48 | accept: "application/json", 49 | }, 50 | }); 51 | axiosRetry(this.apiClient, { 52 | retries: 5, 53 | retryCondition: e => e.response?.status === 429, 54 | retryDelay: axiosRetry.exponentialDelay, 55 | onRetry: (_, e) => { 56 | this.log.debug({ statusCode: e.status, data: e.response?.data }); 57 | }, 58 | }); 59 | this.log.debug(`API URL: ${baseURL}`); 60 | try { 61 | const resp = await this.apiClient.get("/approve/spender"); 62 | this.routerAddress = resp.data.address; 63 | this.log.info(`1inch router address: ${this.routerAddress}`); 64 | } catch (e) { 65 | this.log.error(`failed to get router address: ${e}`); 66 | } 67 | } 68 | 69 | public async swap(tokenAddr: Address, amount: bigint): Promise { 70 | const amnt = formatBN(amount, getDecimals(tokenAddr)); 71 | let transactionHash: string | undefined; 72 | if (amount <= 10n) { 73 | this.log.debug( 74 | `skip swapping ${amount} ${tokenSymbolByAddress[tokenAddr]} back to ETH: amount to small`, 75 | ); 76 | return; 77 | } 78 | try { 79 | if (tokenAddr.toLowerCase() === this.wethAddr.toLowerCase()) { 80 | // WETH is unwrapped during liquidation (convertWETH flag) 81 | return; 82 | } 83 | this.log.debug( 84 | `swapping ${amnt} ${tokenSymbolByAddress[tokenAddr]} back to ETH`, 85 | ); 86 | await this.client.simulateAndWrite({ 87 | abi: ierc20MetadataAbi, 88 | address: tokenAddr, 89 | functionName: "approve", 90 | args: [this.routerAddress, amount], 91 | }); 92 | 93 | const swap = await this.apiClient.get("/swap", { 94 | params: { 95 | src: tokenAddr, 96 | dst: ETH, 97 | amount: amount.toString(), 98 | from: this.client.address, 99 | slippage: this.slippage, 100 | disableEstimate: true, 101 | allowPartialFill: false, 102 | receiver: this.client.address, 103 | }, 104 | }); 105 | 106 | // TODO: this was not tested after viem rewrite 107 | const { 108 | tx: { gas, gasPrice, ...tx }, 109 | // ...rest 110 | } = swap.data; 111 | const transactionHash = await this.client.wallet.sendTransaction(tx); 112 | await this.client.pub.waitForTransactionReceipt({ 113 | hash: transactionHash, 114 | timeout: 120_000, 115 | }); 116 | this.log.debug( 117 | `swapped ${amnt} ${tokenSymbolByAddress[tokenAddr]} back to ETH`, 118 | ); 119 | } catch (e) { 120 | let info: any; 121 | if (axios.isAxiosError(e)) { 122 | info = e.response?.data?.description; 123 | } 124 | info = info || `${e}`; 125 | const error = new OneInchError( 126 | `failed to swap ${amnt} ${tokenSymbolByAddress[tokenAddr]} back to ETH: ${info}`, 127 | ); 128 | error.transactionHash = transactionHash; 129 | throw error; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/services/swap/types.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import type { Address } from "viem"; 3 | /** 4 | * Service that used to swap underlying back to ETH after liquidation 5 | */ 6 | export interface ISwapper { 7 | launch: (network: NetworkType) => Promise; 8 | swap: (tokenAddr: Address, amount: bigint) => Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/services/swap/uniswap.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { 3 | CHAINS, 4 | decimals, 5 | getDecimals, 6 | tokenSymbolByAddress, 7 | } from "@gearbox-protocol/sdk-gov"; 8 | import { ierc20MetadataAbi } from "@gearbox-protocol/types/abi"; 9 | import type { Currency } from "@uniswap/sdk-core"; 10 | import { CurrencyAmount, Percent, Token, TradeType } from "@uniswap/sdk-core"; 11 | import IUniswapV3PoolABI from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json" with { type: "json" }; 12 | import type { SwapOptions } from "@uniswap/v3-sdk"; 13 | import { 14 | computePoolAddress, 15 | FeeAmount, 16 | Pool, 17 | Route, 18 | SwapQuoter, 19 | SwapRouter, 20 | Trade, 21 | } from "@uniswap/v3-sdk"; 22 | import type { Address, Hex } from "viem"; 23 | import { 24 | decodeAbiParameters, 25 | fromHex, 26 | getContract, 27 | parseAbiParameters, 28 | } from "viem"; 29 | 30 | import type { ILogger } from "../../log/index.js"; 31 | import { Logger } from "../../log/index.js"; 32 | import BaseSwapper from "./base.js"; 33 | import type { ISwapper } from "./types.js"; 34 | 35 | const QUOTER_CONTRACT_ADDRESS: Address = 36 | "0x61fFE014bA17989E743c5F6cB21bF9697530B21e"; 37 | const POOL_FACTORY_CONTRACT_ADDRESS: Address = 38 | "0x1F98431c8aD98523631AE4a59f267346ea31F984"; 39 | const SWAP_ROUTER_ADDRESS: Address = 40 | "0xE592427A0AEce92De3Edee1F18E0157C05861564"; 41 | 42 | export default class Uniswap extends BaseSwapper implements ISwapper { 43 | @Logger("uniswap") 44 | log!: ILogger; 45 | 46 | private WETH!: Token; 47 | 48 | // TODO: this was not tested after View rewrite 49 | public async launch(network: NetworkType): Promise { 50 | await super.launch(network); 51 | this.WETH = new Token( 52 | CHAINS[network], 53 | this.wethAddr, 54 | decimals.WETH, 55 | "WETH", 56 | "Wrapped Ether", 57 | ); 58 | } 59 | 60 | public async swap(tokenAddr: Address, amount: bigint): Promise { 61 | if (amount <= 10n) { 62 | this.log.debug( 63 | `skip swapping ${amount} ${tokenSymbolByAddress[tokenAddr]} back to ETH: amount to small`, 64 | ); 65 | return; 66 | } 67 | try { 68 | if (tokenAddr.toLowerCase() !== this.wethAddr.toLowerCase()) { 69 | this.log.debug( 70 | `swapping ${tokenSymbolByAddress[tokenAddr]} back to ETH`, 71 | ); 72 | await this.executeTrade(tokenAddr, amount); 73 | this.log.debug(`swapped ${tokenSymbolByAddress[tokenAddr]} to WETH`); 74 | } 75 | this.log.debug("unwrapped ETH"); 76 | } catch (e) { 77 | this.log.error( 78 | `gailed to swap ${tokenSymbolByAddress[tokenAddr]} back to ETH: ${e}`, 79 | ); 80 | } 81 | } 82 | 83 | private async executeTrade( 84 | tokenAddr: Address, 85 | amount: bigint, 86 | ): Promise { 87 | const token = new Token( 88 | CHAINS[this.network], 89 | tokenAddr, 90 | getDecimals(tokenAddr), 91 | tokenSymbolByAddress[tokenAddr], 92 | tokenSymbolByAddress[tokenAddr], 93 | ); 94 | 95 | const pool = await this.getPool(token); 96 | const swapRoute = new Route([pool], token, this.WETH); 97 | const amountOut = await this.getOutputQuote(token, amount, swapRoute); 98 | 99 | const trade = Trade.createUncheckedTrade({ 100 | route: swapRoute, 101 | inputAmount: CurrencyAmount.fromRawAmount(token, amount.toString()), 102 | outputAmount: CurrencyAmount.fromRawAmount( 103 | this.WETH, 104 | amountOut.toString(), 105 | ), 106 | tradeType: TradeType.EXACT_INPUT, 107 | }); 108 | 109 | await this.client.simulateAndWrite({ 110 | address: token.address as Address, 111 | abi: ierc20MetadataAbi, 112 | functionName: "approve", 113 | args: [SWAP_ROUTER_ADDRESS, amount], 114 | }); 115 | 116 | const options: SwapOptions = { 117 | slippageTolerance: new Percent(50, 10_000), // 50 bips, or 0.50% 118 | deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from the current Unix time 119 | recipient: this.client.address, 120 | }; 121 | 122 | const methodParameters = SwapRouter.swapCallParameters([trade], options); 123 | 124 | await this.client.wallet.sendTransaction({ 125 | data: methodParameters.calldata as Hex, 126 | to: SWAP_ROUTER_ADDRESS, 127 | value: fromHex(methodParameters.value as Hex, "bigint"), 128 | from: this.client.address, 129 | }); 130 | } 131 | 132 | private async getPool(token: Token): Promise { 133 | const currentPoolAddress = computePoolAddress({ 134 | factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS, 135 | tokenA: token, 136 | tokenB: this.WETH, 137 | fee: FeeAmount.MEDIUM, 138 | }) as Address; 139 | 140 | const poolContract = getContract({ 141 | abi: IUniswapV3PoolABI.abi, 142 | address: currentPoolAddress, 143 | client: this.client.pub, 144 | }); 145 | 146 | const [liquidity, slot0] = (await Promise.all([ 147 | poolContract.read.liquidity(), 148 | poolContract.read.slot0(), 149 | ])) as [any, any]; 150 | 151 | return new Pool( 152 | token, 153 | this.WETH, 154 | FeeAmount.MEDIUM, 155 | slot0[0].toString(), 156 | liquidity, 157 | slot0[1], 158 | ); 159 | } 160 | 161 | private async getOutputQuote( 162 | token: Token, 163 | amount: bigint, 164 | route: Route, 165 | ): Promise { 166 | const { calldata } = SwapQuoter.quoteCallParameters( 167 | route, 168 | CurrencyAmount.fromRawAmount(token, amount.toString()), 169 | TradeType.EXACT_INPUT, 170 | { 171 | useQuoterV2: true, 172 | }, 173 | ); 174 | const { data: quoteCallReturnData } = await this.client.pub.call({ 175 | to: QUOTER_CONTRACT_ADDRESS, 176 | data: calldata as Hex, 177 | }); 178 | 179 | const [amountOut] = decodeAbiParameters( 180 | parseAbiParameters("uint256"), 181 | quoteCallReturnData!, 182 | ); 183 | 184 | return amountOut; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/utils/bigint-serializer.ts: -------------------------------------------------------------------------------- 1 | // Wrapper around JSON stringify/parse methods to support bigint serialization 2 | 3 | function replacer(key: string, value: any) { 4 | if (typeof value === "bigint") { 5 | return { 6 | __type: "bigint", 7 | __value: value.toString(), 8 | }; 9 | } else { 10 | return value; 11 | } 12 | } 13 | 14 | function reviver(key: string, value: any) { 15 | if (value && value.__type === "bigint") { 16 | return BigInt(value.__value); 17 | } 18 | return value; 19 | } 20 | 21 | export const json_stringify = (obj: any) => { 22 | return JSON.stringify(obj, replacer, 2); 23 | }; 24 | 25 | export const json_parse = (s: string) => { 26 | return JSON.parse(s, reviver); 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/bigint-utils.ts: -------------------------------------------------------------------------------- 1 | export class BigIntUtils { 2 | static max(a: bigint, b: bigint): bigint { 3 | return a > b ? a : b; 4 | } 5 | 6 | static min(a: bigint, b: bigint): bigint { 7 | return a < b ? a : b; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/detect-network.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { supportedChains, tokenDataByNetwork } from "@gearbox-protocol/sdk-gov"; 3 | import { ierc20MetadataAbi } from "@gearbox-protocol/types/abi"; 4 | import type { Address, PublicClient } from "viem"; 5 | 6 | function wellKnownTokenFor(network: NetworkType): Address { 7 | if (network === "Sonic") { 8 | return tokenDataByNetwork[network].USDC_e; 9 | } 10 | return tokenDataByNetwork[network].USDC; 11 | } 12 | 13 | export async function detectNetwork( 14 | client: PublicClient, 15 | ): Promise { 16 | for (const chain of supportedChains) { 17 | try { 18 | await client.readContract({ 19 | abi: ierc20MetadataAbi, 20 | address: wellKnownTokenFor(chain), 21 | functionName: "symbol", 22 | }); 23 | return chain; 24 | } catch {} 25 | } 26 | 27 | throw new Error("Unsupported network"); 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/balancerVault.ts: -------------------------------------------------------------------------------- 1 | export enum PoolSpecialization { 2 | GeneralPool = 0, 3 | MinimalSwapInfoPool, 4 | TwoTokenPool, 5 | } 6 | 7 | /** 8 | * Splits a poolId into its components, i.e. pool address, pool specialization and its nonce 9 | * @param poolId - a bytes32 string of the pool's ID 10 | * @returns an object with the decomposed poolId 11 | */ 12 | export const splitPoolId = (poolId: string) => { 13 | return { 14 | address: getPoolAddress(poolId), 15 | specialization: getPoolSpecialization(poolId), 16 | nonce: getPoolNonce(poolId), 17 | }; 18 | }; 19 | 20 | /** 21 | * Extracts a pool's address from its poolId 22 | * @param poolId - a bytes32 string of the pool's ID 23 | * @returns the pool's address 24 | */ 25 | export const getPoolAddress = (poolId: string): string => { 26 | if (poolId.length !== 66) throw new Error("Invalid poolId length"); 27 | return poolId.slice(0, 42); 28 | }; 29 | 30 | /** 31 | * Extracts a pool's specialization from its poolId 32 | * @param poolId - a bytes32 string of the pool's ID 33 | * @returns the pool's specialization 34 | */ 35 | export const getPoolSpecialization = (poolId: string): PoolSpecialization => { 36 | if (poolId.length !== 66) throw new Error("Invalid poolId length"); 37 | 38 | // Only have 3 pool specializations so we can just pull the relevant character 39 | const specializationCode = parseInt(poolId[45], 10); 40 | if (specializationCode >= 3) throw new Error("Invalid pool specialization"); 41 | 42 | return specializationCode; 43 | }; 44 | 45 | /** 46 | * Extracts a pool's nonce from its poolId 47 | * @param poolId - a bytes32 string of the pool's ID 48 | * @returns the pool's nonce 49 | */ 50 | export const getPoolNonce = (poolId: string): bigint => { 51 | if (poolId.length !== 66) throw new Error("Invalid poolId length"); 52 | return BigInt(`0x${poolId.slice(46)}`); 53 | }; 54 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/core.ts: -------------------------------------------------------------------------------- 1 | import type { Address } from "viem"; 2 | 3 | import type { MultiCall } from "../../../data/MultiCall.js"; 4 | 5 | export enum SwapOperation { 6 | EXACT_INPUT, 7 | EXACT_INPUT_ALL, 8 | EXACT_OUTPUT, 9 | } 10 | 11 | export interface PathFinderResult { 12 | amount: bigint; 13 | minAmount: bigint; 14 | calls: MultiCall[]; 15 | } 16 | 17 | export interface PathFinderOpenStrategyResult extends PathFinderResult { 18 | balances: Record; 19 | minBalances: Record; 20 | } 21 | 22 | export interface PathFinderCloseResult extends PathFinderResult { 23 | underlyingBalance: bigint; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core.js"; 2 | export * from "./pathfinder.js"; 3 | export * from "./pathOptions.js"; 4 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/pathOptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AuraLPToken, 3 | AuraStakedToken, 4 | BalancerLPToken, 5 | ConvexLPToken, 6 | CurveLPToken, 7 | CurveParams, 8 | NetworkType, 9 | YearnLPToken, 10 | } from "@gearbox-protocol/sdk-gov"; 11 | import { 12 | auraTokens, 13 | balancerLpTokens, 14 | contractParams, 15 | convexTokens, 16 | curveTokens, 17 | isBalancerLPToken, 18 | isCurveLPToken, 19 | toBigInt, 20 | tokenDataByNetwork, 21 | tokenSymbolByAddress, 22 | yearnTokens, 23 | } from "@gearbox-protocol/sdk-gov"; 24 | import type { Address } from "viem"; 25 | 26 | import type { TokenBalance } from "../../../data/index.js"; 27 | 28 | export interface PathOption { 29 | target: Address; 30 | option: number; 31 | totalOptions: number; 32 | } 33 | 34 | export type PathOptionSerie = PathOption[]; 35 | 36 | export class PathOptionFactory { 37 | static generatePathOptions( 38 | balances: Array>, 39 | loopsInTx: number, 40 | network: NetworkType, 41 | ): Array { 42 | const curvePools = PathOptionFactory.getCurvePools(balances); 43 | const balancerPools = PathOptionFactory.getBalancerPools(balances); 44 | 45 | const curveInitPO: PathOptionSerie = curvePools.map(symbol => { 46 | return { 47 | target: tokenDataByNetwork[network][symbol], 48 | option: 0, 49 | totalOptions: (contractParams[curveTokens[symbol].pool] as CurveParams) 50 | .tokens.length, 51 | }; 52 | }); 53 | const balancerInitPO: PathOptionSerie = balancerPools.map(symbol => { 54 | return { 55 | target: tokenDataByNetwork[network][symbol], 56 | option: 0, 57 | totalOptions: balancerLpTokens[symbol].underlying.length, 58 | }; 59 | }); 60 | const initPO = [...curveInitPO, ...balancerInitPO]; 61 | 62 | const totalLoops = initPO.reduce( 63 | (acc, item) => acc * item.totalOptions, 64 | 1, 65 | ); 66 | 67 | const result: Array = []; 68 | 69 | let currentPo = [...initPO]; 70 | 71 | for (let i = 0; i < totalLoops; i++) { 72 | if (i % loopsInTx === 0) { 73 | result.push(currentPo); 74 | } 75 | if (i < totalLoops - 1) { 76 | currentPo = PathOptionFactory.next(currentPo); 77 | } 78 | } 79 | 80 | return result; 81 | } 82 | 83 | static getCurvePools( 84 | balances: Array>, 85 | ): Array { 86 | const nonZeroBalances = balances.filter(b => toBigInt(b.balance) > 1); 87 | 88 | const curvePools = nonZeroBalances 89 | .map(b => tokenSymbolByAddress[b.token.toLowerCase()]) 90 | .filter(symbol => isCurveLPToken(symbol)) as Array; 91 | 92 | const yearnCurveTokens = Object.entries(yearnTokens) 93 | .filter(([, data]) => isCurveLPToken(data.underlying)) 94 | .map(([token]) => token); 95 | 96 | const curvePoolsFromYearn = nonZeroBalances 97 | .map(b => tokenSymbolByAddress[b.token.toLowerCase()]) 98 | .filter(symbol => yearnCurveTokens.includes(symbol)) 99 | .map( 100 | symbol => yearnTokens[symbol as YearnLPToken].underlying, 101 | ) as Array; 102 | 103 | const convexCurveTokens = Object.entries(convexTokens) 104 | .filter(([, data]) => isCurveLPToken(data.underlying)) 105 | .map(([token]) => token); 106 | 107 | const curvePoolsFromConvex = nonZeroBalances 108 | .map(b => tokenSymbolByAddress[b.token.toLowerCase()]) 109 | .filter(symbol => convexCurveTokens.includes(symbol)) 110 | .map(symbol => convexTokens[symbol as ConvexLPToken].underlying); 111 | 112 | const curveSet = new Set([ 113 | ...curvePools, 114 | ...curvePoolsFromYearn, 115 | ...curvePoolsFromConvex, 116 | ]); 117 | return Array.from(curveSet.values()); 118 | } 119 | 120 | static getBalancerPools( 121 | balances: Array>, 122 | ): Array { 123 | const nonZeroBalances = balances.filter(b => toBigInt(b.balance) > 1); 124 | 125 | const balancerPools = nonZeroBalances 126 | .map(b => tokenSymbolByAddress[b.token.toLowerCase()]) 127 | .filter(symbol => isBalancerLPToken(symbol)) as Array; 128 | 129 | const balancerAuraTokens = Object.entries(auraTokens) 130 | .filter(([, data]) => isBalancerLPToken(data.underlying)) 131 | .map(([token]) => token); 132 | 133 | const balancerTokensFromAura = nonZeroBalances 134 | .map(b => tokenSymbolByAddress[b.token.toLowerCase()]) 135 | .filter(symbol => balancerAuraTokens.includes(symbol)) 136 | .map( 137 | symbol => 138 | auraTokens[symbol as AuraLPToken | AuraStakedToken].underlying, 139 | ); 140 | 141 | const balancerSet = new Set([...balancerPools, ...balancerTokensFromAura]); 142 | 143 | return Array.from(balancerSet.values()); 144 | } 145 | 146 | static next(path: PathOptionSerie): PathOptionSerie { 147 | let newPath = [...path]; 148 | for (let i = path.length - 1; i >= 0; i--) { 149 | const po = { ...newPath[i] }; 150 | po.option++; 151 | newPath[i] = po; 152 | 153 | if (po.option < po.totalOptions) return newPath; 154 | po.option = 0; 155 | } 156 | 157 | throw new Error("Path options overflow"); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/pathfinder.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import { 3 | getConnectors, 4 | getDecimals, 5 | getTokenSymbol, 6 | tokenDataByNetwork, 7 | } from "@gearbox-protocol/sdk-gov"; 8 | import { iRouterV3Abi } from "@gearbox-protocol/types/abi"; 9 | import { type Address, getContract, type PublicClient } from "viem"; 10 | 11 | import type { 12 | Balance, 13 | CreditAccountData, 14 | CreditManagerData, 15 | } from "../../../data/index.js"; 16 | import type { ILogger } from "../../../log/index.js"; 17 | import { Logger } from "../../../log/index.js"; 18 | import type { PathFinderCloseResult } from "./core.js"; 19 | import type { PathOptionSerie } from "./pathOptions.js"; 20 | import { PathOptionFactory } from "./pathOptions.js"; 21 | import type { 22 | EstimateBatchInput, 23 | IRouterV3Contract, 24 | RouterResult, 25 | } from "./viem-types.js"; 26 | 27 | const MAX_GAS_PER_ROUTE = 200_000_000n; 28 | const GAS_PER_BLOCK = 400_000_000n; 29 | const LOOPS_PER_TX = Number(GAS_PER_BLOCK / MAX_GAS_PER_ROUTE); 30 | 31 | interface FindBestClosePathInterm { 32 | pathOptions: PathOptionSerie[]; 33 | expected: Balance[]; 34 | leftover: Balance[]; 35 | connectors: Address[]; 36 | } 37 | 38 | export class PathFinder { 39 | @Logger("PathFinder") 40 | logger!: ILogger; 41 | 42 | readonly #pathFinder: IRouterV3Contract; 43 | readonly #connectors: Set
; 44 | readonly #network: NetworkType; 45 | 46 | constructor(address: Address, client: PublicClient, network: NetworkType) { 47 | this.#pathFinder = getContract({ 48 | abi: iRouterV3Abi, 49 | address, 50 | client, 51 | }); 52 | this.#network = network; 53 | this.#connectors = new Set( 54 | getConnectors(network).map(c => c.toLowerCase() as Address), 55 | ); 56 | } 57 | 58 | /** 59 | * @dev Finds the path to swap / withdraw all assets from CreditAccount into underlying asset 60 | * Can bu used for closing Credit Account and for liquidations as well. 61 | * @param ca CreditAccountData object used for close path computation 62 | * @param cm CreditManagerData for corresponging credit manager 63 | * @param slippage Slippage in PERCENTAGE_FORMAT (100% = 10_000) per operation 64 | * @return The best option in PathFinderCloseResult format, which 65 | * - underlyingBalance - total balance of underlying token 66 | * - calls - list of calls which should be done to swap & unwrap everything to underlying token 67 | */ 68 | async findBestClosePath( 69 | ca: CreditAccountData, 70 | cm: CreditManagerData, 71 | slippage: bigint | number, 72 | ): Promise { 73 | const { pathOptions, expected, leftover, connectors } = 74 | this.#getBestClosePathInput(ca, cm); 75 | const logger = this.logger.child({ 76 | account: ca.addr, 77 | borrower: ca.borrower, 78 | manager: ca.managerName, 79 | }); 80 | logger.debug( 81 | `connectors: ${connectors.map(c => getTokenSymbol(c)).join(", ")}`, 82 | ); 83 | // TODO: stkcvxRLUSDUSDC workaround 84 | const force = ca.allBalances.some( 85 | b => 86 | b.token.toLowerCase() === 87 | tokenDataByNetwork.Mainnet.stkcvxRLUSDUSDC.toLowerCase() && 88 | b.balance > 10n, 89 | ); 90 | if (force) { 91 | logger.warn("applying stkcvxRLUSDUSDC workaround"); 92 | } 93 | let results: RouterResult[] = []; 94 | for (const po of pathOptions) { 95 | const { result } = await this.#pathFinder.simulate.findBestClosePath( 96 | [ 97 | ca.addr, 98 | expected, 99 | leftover, 100 | connectors, 101 | BigInt(slippage), 102 | po, 103 | BigInt(LOOPS_PER_TX), 104 | force, 105 | ], 106 | { 107 | gas: GAS_PER_BLOCK, 108 | }, 109 | ); 110 | results.push(result); 111 | } 112 | 113 | const bestResult = results.reduce( 114 | (best, pathFinderResult) => PathFinder.compare(best, pathFinderResult), 115 | { 116 | amount: 0n, 117 | minAmount: 0n, 118 | calls: [], 119 | }, 120 | ); 121 | 122 | return { 123 | amount: bestResult.amount, 124 | minAmount: bestResult.minAmount, 125 | calls: bestResult.calls.map(c => ({ 126 | callData: c.callData, 127 | target: c.target, 128 | })), 129 | underlyingBalance: bestResult.minAmount + ca.balances[ca.underlyingToken], 130 | }; 131 | } 132 | 133 | // TODO: readme 134 | getEstimateBatchInput( 135 | ca: CreditAccountData, 136 | cm: CreditManagerData, 137 | slippage: number, 138 | ): EstimateBatchInput { 139 | const { pathOptions, connectors, expected, leftover } = 140 | this.#getBestClosePathInput(ca, cm); 141 | return { 142 | creditAccount: ca.addr, 143 | expectedBalances: expected, 144 | leftoverBalances: leftover, 145 | connectors, 146 | slippage: BigInt(slippage), 147 | pathOptions: pathOptions[0] ?? [], // TODO: what to put here? 148 | iterations: BigInt(LOOPS_PER_TX), 149 | force: false, 150 | priceUpdates: [], 151 | }; 152 | } 153 | 154 | #getBestClosePathInput( 155 | ca: CreditAccountData, 156 | cm: CreditManagerData, 157 | ): FindBestClosePathInterm { 158 | const expectedBalances: Record = {}; 159 | const leftoverBalances: Record = {}; 160 | for (const { token, balance, isEnabled } of ca.allBalances) { 161 | expectedBalances[token] = { token, balance }; 162 | // filter out dust, we don't want to swap it 163 | const minBalance = 10n ** BigInt(Math.max(8, getDecimals(token)) - 8); 164 | // also: gearbox liquidator does not need to swap disabled tokens. third-party liquidators might want to do it 165 | if (balance < minBalance || !isEnabled) { 166 | leftoverBalances[token] = { token, balance }; 167 | } 168 | 169 | // TODO: this was not tested, revert 170 | // if (balance < minBalance) { 171 | // // According to van0k: 172 | // // If the token is enabled, we need to pass the exact balance, even if it's 0 173 | // // If it's not enabled, we can set it to 1 event if the balance is 0 174 | // leftoverBalances[token] = { 175 | // token, 176 | // balance: isEnabled ? balance : BigIntUtils.max(1n, balance), 177 | // }; 178 | // } 179 | } 180 | 181 | const pathOptions = PathOptionFactory.generatePathOptions( 182 | ca.allBalances, 183 | LOOPS_PER_TX, 184 | this.#network, 185 | ); 186 | 187 | const expected: Balance[] = cm.collateralTokens.map(token => { 188 | // When we pass expected balances explicitly, we need to mimic router behaviour by filtering out leftover tokens 189 | // for example, we can have stETH balance of 2, because 1 transforms to 2 because of rebasing 190 | // https://github.com/Gearbox-protocol/router-v3/blob/c230a3aa568bb432e50463cfddc877fec8940cf5/contracts/RouterV3.sol#L222 191 | const actual = expectedBalances[token]?.balance || 0n; 192 | return { 193 | token, 194 | balance: actual > 10n ? actual : 0n, 195 | }; 196 | }); 197 | 198 | const leftover: Balance[] = cm.collateralTokens.map(token => ({ 199 | token, 200 | balance: leftoverBalances[token]?.balance || 1n, 201 | })); 202 | 203 | const connectors = this.getAvailableConnectors(cm.collateralTokens); 204 | return { expected, leftover, connectors, pathOptions }; 205 | } 206 | 207 | static compare(r1: T, r2: T): T { 208 | return r1.amount > r2.amount ? r1 : r2; 209 | } 210 | 211 | public getAvailableConnectors(tokens: Address[]): Address[] { 212 | return tokens.filter(t => this.#connectors.has(t.toLowerCase() as Address)); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/pathfinder/viem-types.ts: -------------------------------------------------------------------------------- 1 | import type { iBatchLiquidatorAbi } from "@gearbox-protocol/liquidator-v2-contracts/abi"; 2 | import type { iRouterV3Abi } from "@gearbox-protocol/types/abi"; 3 | import type { AbiParameterToPrimitiveType, ExtractAbiFunction } from "abitype"; 4 | import type { GetContractReturnType, PublicClient } from "viem"; 5 | 6 | import type { ArrayElementType } from "../../index.js"; 7 | 8 | export type IRouterV3Contract = GetContractReturnType< 9 | typeof iRouterV3Abi, 10 | PublicClient 11 | >; 12 | 13 | export type RouterResult = AbiParameterToPrimitiveType< 14 | ExtractAbiFunction["outputs"]["0"] 15 | >; 16 | 17 | export type EstimateBatchInput = ArrayElementType< 18 | AbiParameterToPrimitiveType< 19 | ExtractAbiFunction< 20 | typeof iBatchLiquidatorAbi, 21 | "estimateBatch" 22 | >["inputs"]["0"] 23 | > 24 | >; 25 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/ERC20Parser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedToken } from "@gearbox-protocol/sdk-gov"; 2 | import { toBigInt } from "@gearbox-protocol/sdk-gov"; 3 | import { IERC20__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class ERC20Parser extends AbstractParser implements IParser { 9 | constructor(symbol: SupportedToken) { 10 | super(symbol); 11 | this.adapterName = "Token"; 12 | this.ifc = IERC20__factory.createInterface(); 13 | } 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | case "totalSupply": { 19 | return `${functionName}()`; 20 | } 21 | case "balanceOf": { 22 | const [address] = this.decodeFunctionData(functionFragment, calldata); 23 | return `${functionName}(${address})`; 24 | } 25 | case "allowance": { 26 | const [account, to] = this.decodeFunctionData( 27 | functionFragment, 28 | calldata, 29 | ); 30 | return `${functionName}(account: ${account}, to: ${to})`; 31 | } 32 | 33 | case "approve": { 34 | const [spender, amount] = this.decodeFunctionData( 35 | functionFragment, 36 | calldata, 37 | ); 38 | return `${functionName}(${spender}, [${toBigInt(amount).toString()}])`; 39 | } 40 | 41 | default: 42 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/MellowLrtVaultAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IMellowVaultAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class MellowLrtVaultAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IMellowVaultAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "MellowLRTVaultAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | return this._parse(calldata); 19 | } 20 | 21 | protected _parse(calldata: string): string { 22 | const { functionFragment, functionName } = this.parseSelector(calldata); 23 | 24 | switch (functionFragment.name) { 25 | default: 26 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/PendleRouterAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IPendleRouterAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class PendleRouterAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IPendleRouterAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "PendleRouterAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | return this._parse(calldata); 19 | } 20 | 21 | protected _parse(calldata: string): string { 22 | const { functionFragment, functionName } = this.parseSelector(calldata); 23 | 24 | switch (functionFragment.name) { 25 | default: 26 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/TxParserHelper.ts: -------------------------------------------------------------------------------- 1 | import { ADDRESS_0X0, tokenSymbolByAddress } from "@gearbox-protocol/sdk-gov"; 2 | import type { MultiCall } from "@gearbox-protocol/types/v3"; 3 | 4 | import type { CreditManagerData } from "../../../data/index.js"; 5 | import { TxParser } from "./txParser.js"; 6 | 7 | export class TxParserHelper { 8 | /** 9 | * This is helper for legacy code 10 | * in old versions of "@gearbox-protocol/sdk" where TxParser originally lives, this code is called from CreditManagerData constructor (!!) 11 | * @param cm 12 | */ 13 | public static addCreditManager(cm: CreditManagerData): void { 14 | TxParser.addCreditManager(cm.address, cm.version); 15 | 16 | if (!!cm.creditFacade && cm.creditFacade !== ADDRESS_0X0) { 17 | TxParser.addCreditFacade( 18 | cm.creditFacade, 19 | tokenSymbolByAddress[cm.underlyingToken], 20 | cm.version, 21 | ); 22 | 23 | TxParser.addAdapters( 24 | Object.entries(cm.adapters).map(([contract, adapter]) => ({ 25 | adapter, 26 | contract: contract, 27 | })), 28 | ); 29 | } 30 | } 31 | 32 | public static parseMultiCall(preview: { calls: MultiCall[] }): string[] { 33 | try { 34 | return TxParser.parseMultiCall(preview.calls); 35 | } catch (e) { 36 | return [`${e}`]; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/aaveV2LendingPoolAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IAaveV2_LendingPoolAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class AaveV2LendingPoolAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IAaveV2_LendingPoolAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "AaveV2_LendingPoolAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | const { functionFragment, functionName } = this.parseSelector(calldata); 19 | 20 | switch (functionFragment.name) { 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/aaveV2WrappedATokenAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IAaveV2_WrappedATokenAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class AaveV2WrappedATokenAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IAaveV2_WrappedATokenAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "AaveV2_WrappedATokenAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | const { functionFragment, functionName } = this.parseSelector(calldata); 19 | 20 | switch (functionFragment.name) { 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/abstractParser.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Address, 3 | SupportedToken, 4 | TickerToken, 5 | } from "@gearbox-protocol/sdk-gov"; 6 | import { 7 | decimals, 8 | formatBN, 9 | getTokenSymbolOrTicker, 10 | toBigInt, 11 | tokenSymbolByAddress, 12 | } from "@gearbox-protocol/sdk-gov"; 13 | import type { 14 | BigNumberish, 15 | BytesLike, 16 | FunctionFragment, 17 | Interface, 18 | Result, 19 | } from "ethers"; 20 | import { ethers } from "ethers"; 21 | 22 | interface ParseSelectorResult { 23 | functionFragment: FunctionFragment; 24 | functionName: string; 25 | } 26 | 27 | export class AbstractParser { 28 | public readonly contract: string; 29 | protected ifc!: Interface; 30 | public adapterName: string; 31 | 32 | constructor(contract: string) { 33 | this.contract = contract; 34 | this.adapterName = "Contract"; 35 | } 36 | 37 | parseSelector(calldata: BytesLike): ParseSelectorResult { 38 | const functionFragment = this.ifc.getFunction( 39 | ethers.dataSlice(calldata, 0, 4) as any, 40 | )!; 41 | 42 | const functionName = `${this.adapterName}[${this.contract}].${functionFragment.name}`; 43 | return { functionFragment, functionName }; 44 | } 45 | 46 | decodeFunctionData( 47 | functionFragment: FunctionFragment | string, 48 | data: BytesLike, 49 | ): Result { 50 | return this.ifc.decodeFunctionData(functionFragment, data); 51 | } 52 | 53 | encodeFunctionResult( 54 | functionFragment: FunctionFragment | string, 55 | data: Array, 56 | ) { 57 | return this.ifc.encodeFunctionResult(functionFragment, data); 58 | } 59 | 60 | tokenSymbol(address: string): SupportedToken { 61 | const symbol = tokenSymbolByAddress[address.toLowerCase()]; 62 | if (!symbol) throw new Error(`Unknown token: ${address}`); 63 | return symbol; 64 | } 65 | 66 | tokenOrTickerSymbol(address: string): SupportedToken | TickerToken { 67 | const symbol = getTokenSymbolOrTicker(address as Address); 68 | if (!symbol) { 69 | throw new Error(`Unknown token or ticker: ${address}`); 70 | } 71 | return symbol; 72 | } 73 | 74 | formatBN(amount: BigNumberish, token: SupportedToken): string { 75 | return `${formatBN(amount, decimals[token])} [${toBigInt( 76 | amount, 77 | ).toString()}]`; 78 | } 79 | 80 | parseToObject(address: string, calldata: string) { 81 | const { functionFragment } = this.parseSelector(calldata); 82 | 83 | const args = this.decodeFunctionData(functionFragment, calldata); 84 | 85 | return { 86 | address, 87 | functionFragment, 88 | args, 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/addressProviderParser.ts: -------------------------------------------------------------------------------- 1 | import { IAddressProviderV3__factory } from "@gearbox-protocol/types/v3"; 2 | 3 | import { AbstractParser } from "./abstractParser.js"; 4 | import type { IParser } from "./iParser.js"; 5 | 6 | export class AddressProviderParser extends AbstractParser implements IParser { 7 | constructor() { 8 | super("AddressProvider"); 9 | this.ifc = IAddressProviderV3__factory.createInterface(); 10 | } 11 | parse(calldata: string): string { 12 | const { functionFragment, functionName } = this.parseSelector(calldata); 13 | 14 | switch (functionFragment.name) { 15 | case "getWethToken": 16 | case "getGearToken": 17 | case "getLeveragedActions": 18 | case "getDataCompressor": 19 | case "getWETHGateway": 20 | case "getPriceOracle": { 21 | return `${functionName}()`; 22 | } 23 | 24 | default: 25 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/balancerV2VaultParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import type { 3 | SingleSwapDiffStructOutput, 4 | SingleSwapStructOutput, 5 | } from "@gearbox-protocol/types/v3"; 6 | import { IBalancerV2VaultAdapter__factory } from "@gearbox-protocol/types/v3"; 7 | 8 | import { AbstractParser } from "./abstractParser.js"; 9 | import type { IParser } from "./iParser.js"; 10 | 11 | export class BalancerV2VaultParser extends AbstractParser implements IParser { 12 | constructor(contract: SupportedContract, isContract: boolean) { 13 | super(contract); 14 | this.ifc = IBalancerV2VaultAdapter__factory.createInterface(); 15 | if (!isContract) this.adapterName = "BalancerV2Vault"; 16 | } 17 | 18 | parse(calldata: string): string { 19 | const { functionFragment, functionName } = this.parseSelector(calldata); 20 | 21 | switch (functionFragment.name) { 22 | case "batchSwap": { 23 | return `${functionName}(undefined)`; 24 | } 25 | 26 | case "swapDiff": { 27 | const d = this.decodeFunctionData(functionFragment, calldata); 28 | const { 29 | assetIn = "", 30 | assetOut = "", 31 | leftoverAmount = 0, 32 | } = (d?.[0] || {}) as SingleSwapDiffStructOutput; 33 | 34 | return `${functionName}(${this.tokenSymbol( 35 | assetIn, 36 | )} => ${this.tokenSymbol(assetOut)} ${this.formatBN( 37 | leftoverAmount, 38 | this.tokenSymbol(assetIn), 39 | )}}`; 40 | } 41 | 42 | case "swap": { 43 | const d = this.decodeFunctionData(functionFragment, calldata); 44 | const { 45 | assetIn = "", 46 | assetOut = "", 47 | amount = 0, 48 | } = (d?.[0] || {}) as SingleSwapStructOutput; 49 | 50 | return `${functionName}(${this.tokenSymbol( 51 | assetIn, 52 | )} => ${this.tokenSymbol(assetOut)} ${this.formatBN( 53 | amount, 54 | this.tokenSymbol(assetIn), 55 | )}}`; 56 | } 57 | 58 | default: 59 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/camelotV3AdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { formatBN } from "@gearbox-protocol/sdk-gov"; 3 | import { ICamelotV3Adapter__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class CamelotV3AdapterParser extends AbstractParser implements IParser { 9 | constructor(contract: SupportedContract, isContract: boolean) { 10 | super(contract); 11 | this.ifc = ICamelotV3Adapter__factory.createInterface(); 12 | if (!isContract) this.adapterName = "CamelotV3Adapter"; 13 | } 14 | 15 | parse(calldata: string): string { 16 | const { functionFragment, functionName } = this.parseSelector(calldata); 17 | 18 | switch (functionFragment.name) { 19 | case "exactInputSingle": { 20 | const [[tokenIn, tokenOut, fee, , , amountIn, amountOutMinimum]] = 21 | this.decodeFunctionData(functionFragment, calldata); 22 | const tokenInSym = this.tokenSymbol(tokenIn); 23 | const tokenOutSym = this.tokenSymbol(tokenOut); 24 | 25 | const amountInStr = this.formatBN(amountIn, tokenIn); 26 | const amountOutMinimumStr = this.formatBN(amountOutMinimum, tokenOut); 27 | 28 | return `${functionName}(amountIn: ${amountInStr}, amountOutMinimum: ${amountOutMinimumStr}, path: ${tokenInSym} ==(fee: ${fee})==> ${tokenOutSym})`; 29 | } 30 | 31 | case "exactDiffInputSingle": { 32 | const { 33 | params: { tokenIn, tokenOut, leftoverAmount, rateMinRAY }, 34 | } = this.decodeFunctionData(functionFragment, calldata); 35 | const tokenInSym = this.tokenSymbol(tokenIn); 36 | const tokenOutSym = this.tokenSymbol(tokenOut); 37 | 38 | const leftoverAmountStr = this.formatBN(leftoverAmount, tokenIn); 39 | return `${functionName}(leftoverAmount: ${leftoverAmountStr}, rate: ${formatBN( 40 | rateMinRAY, 41 | 27, 42 | )}, path: ${tokenInSym} ==> ${tokenOutSym})`; 43 | } 44 | case "exactInput": { 45 | const { 46 | params: { amountIn, amountOutMinimum, path }, 47 | } = this.decodeFunctionData(functionFragment, calldata); 48 | 49 | const pathStr = this.trackInputPath(path); 50 | const amountInStr = this.formatBN( 51 | amountIn, 52 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 53 | ); 54 | 55 | const amountOutMinimumStr = this.formatBN( 56 | amountOutMinimum, 57 | this.tokenSymbol(`0x${path.slice(-40, path.length)}`), 58 | ); 59 | 60 | return `${functionName}(amountIn: ${amountInStr}, amountOutMinimum: ${amountOutMinimumStr}, path: ${pathStr}`; 61 | } 62 | case "exactDiffInput": { 63 | const { 64 | params: { leftoverAmount, rateMinRAY, path }, 65 | } = this.decodeFunctionData(functionFragment, calldata); 66 | 67 | const leftoverAmountStr = this.formatBN( 68 | leftoverAmount, 69 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 70 | ); 71 | 72 | const pathStr = this.trackInputPath(path); 73 | 74 | return `${functionName}(leftoverAmount: ${leftoverAmountStr}, rate: ${formatBN( 75 | rateMinRAY, 76 | 27, 77 | )}, path: ${pathStr}`; 78 | } 79 | case "exactOutput": { 80 | const { 81 | params: { amountInMaximum, amountOut, path }, 82 | } = this.decodeFunctionData(functionFragment, calldata); 83 | 84 | const pathStr = this.trackOutputPath(path); 85 | const amountInMaximumStr = this.formatBN( 86 | amountInMaximum, 87 | this.tokenSymbol(`0x${path.slice(-40, path.length)}`), 88 | ); 89 | 90 | const amountOutStr = this.formatBN( 91 | amountOut, 92 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 93 | ); 94 | 95 | return `${functionName}(amountInMaximum: ${amountInMaximumStr}, amountOut: ${amountOutStr}, path: ${pathStr}`; 96 | } 97 | case "exactOutputSingle": { 98 | const [[tokenIn, tokenOut, fee, , , amountOut, amountInMaximum]] = 99 | this.decodeFunctionData(functionFragment, calldata); 100 | 101 | const tokenInSym = this.tokenSymbol(tokenIn); 102 | const tokenOutSym = this.tokenSymbol(tokenOut); 103 | 104 | const amountInMaximumStr = this.formatBN(amountInMaximum, tokenInSym); 105 | const amountOutStr = this.formatBN(amountOut, tokenOutSym); 106 | 107 | return `${functionName}(amountInMaximum: ${amountInMaximumStr}, amountOut: ${amountOutStr}, path: ${tokenInSym} ==(fee: ${fee})==> ${tokenOutSym})`; 108 | } 109 | 110 | default: 111 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 112 | } 113 | } 114 | 115 | trackInputPath(path: string): string { 116 | let result = ""; 117 | let pointer = path.startsWith("0x") ? 2 : 0; 118 | while (pointer <= path.length - 40) { 119 | const from = `0x${path.slice(pointer, pointer + 40)}`.toLowerCase(); 120 | result += this.tokenSymbol(from) || from; 121 | pointer += 40; 122 | 123 | if (pointer > path.length - 6) return result; 124 | 125 | const fee = parseInt(path.slice(pointer, pointer + 6), 16); 126 | 127 | pointer += 6; 128 | result += ` ==(fee: ${fee})==> `; 129 | } 130 | 131 | return result; 132 | } 133 | 134 | trackOutputPath(path: string): string { 135 | let result = ""; 136 | let pointer = path.length; 137 | while (pointer >= 40) { 138 | pointer -= 40; 139 | const from = `0x${path.slice(pointer, pointer + 40)}`; 140 | result += this.tokenSymbol(from) || from; 141 | 142 | if (pointer < 6) return result; 143 | pointer -= 6; 144 | const fee = parseInt(path.slice(pointer, pointer + 6), 16); 145 | 146 | result += ` ==(fee: ${fee})==> `; 147 | } 148 | 149 | return result; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/compoundV2CTokenAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { ICompoundV2_CTokenAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class CompoundV2CTokenAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = ICompoundV2_CTokenAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "CompoundV2_CTokenAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | const { functionFragment, functionName } = this.parseSelector(calldata); 19 | 20 | switch (functionFragment.name) { 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/convexBaseRewardPoolAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConvexPoolParams, 3 | SupportedContract, 4 | } from "@gearbox-protocol/sdk-gov"; 5 | import { contractParams } from "@gearbox-protocol/sdk-gov"; 6 | import { IConvexV1BaseRewardPoolAdapter__factory } from "@gearbox-protocol/types/v3"; 7 | import type { BigNumberish } from "ethers"; 8 | 9 | import { AbstractParser } from "./abstractParser.js"; 10 | import type { IParser } from "./iParser.js"; 11 | 12 | export class ConvexBaseRewardPoolAdapterParser 13 | extends AbstractParser 14 | implements IParser 15 | { 16 | constructor(contract: SupportedContract, isContract: boolean) { 17 | super(contract); 18 | this.ifc = IConvexV1BaseRewardPoolAdapter__factory.createInterface(); 19 | if (!isContract) this.adapterName = "ConvexV1BaseRewardPoolAdapter"; 20 | } 21 | parse(calldata: string): string { 22 | const { functionFragment, functionName } = this.parseSelector(calldata); 23 | 24 | switch (functionFragment.name) { 25 | case "stake": { 26 | const [amount] = this.decodeFunctionData(functionFragment, calldata); 27 | return `${functionName}(amount: ${this.formatAmount(amount)})`; 28 | } 29 | 30 | case "stakeDiff": { 31 | const [leftoverAmount] = this.decodeFunctionData( 32 | functionFragment, 33 | calldata, 34 | ); 35 | return `${functionName}(leftoverAmount: ${this.formatAmount( 36 | leftoverAmount, 37 | )})`; 38 | } 39 | 40 | case "withdraw": 41 | case "withdrawAndUnwrap": { 42 | const [amount, claim] = this.decodeFunctionData( 43 | functionFragment, 44 | calldata, 45 | ); 46 | return `${functionName}(amount: ${this.formatAmount( 47 | amount, 48 | )}, claim: ${claim})`; 49 | } 50 | case "withdrawDiff": 51 | case "withdrawDiffAndUnwrap": { 52 | const [leftoverAmount, claim] = this.decodeFunctionData( 53 | functionFragment, 54 | calldata, 55 | ); 56 | return `${functionName}(leftoverAmount: ${this.formatAmount( 57 | leftoverAmount, 58 | )}, claim: ${claim})`; 59 | } 60 | 61 | case "rewardRate": 62 | return `${functionName}()`; 63 | case "totalSupply": 64 | return `${functionName}()`; 65 | 66 | default: 67 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 68 | } 69 | } 70 | 71 | formatAmount(amount: BigNumberish): string { 72 | return this.formatBN( 73 | amount, 74 | (contractParams[this.contract as SupportedContract] as ConvexPoolParams) 75 | .stakedToken, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/convexBoosterAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { convexLpTokenByPid, convexPoolByPid } from "@gearbox-protocol/sdk-gov"; 3 | import { IConvexV1BoosterAdapter__factory } from "@gearbox-protocol/types/v3"; 4 | import type { BigNumberish } from "ethers"; 5 | 6 | import { AbstractParser } from "./abstractParser.js"; 7 | import type { IParser } from "./iParser.js"; 8 | 9 | export class ConvexBoosterAdapterParser 10 | extends AbstractParser 11 | implements IParser 12 | { 13 | constructor(contract: SupportedContract, isContract: boolean) { 14 | super(contract); 15 | this.ifc = IConvexV1BoosterAdapter__factory.createInterface(); 16 | if (!isContract) this.adapterName = "ConvexV1BoosterAdapter"; 17 | } 18 | parse(calldata: string): string { 19 | const { functionFragment, functionName } = this.parseSelector(calldata); 20 | 21 | switch (functionFragment.name) { 22 | case "deposit": { 23 | const [pid, amount, stake] = this.decodeFunctionData( 24 | functionFragment, 25 | calldata, 26 | ); 27 | return `${functionName}(pid: ${this.formatPid( 28 | pid, 29 | )}, amount: ${this.formatAmount(amount, pid)}, stake: ${stake})`; 30 | } 31 | 32 | case "depositDiff": { 33 | const [pid, leftoverAmount, stake] = this.decodeFunctionData( 34 | functionFragment, 35 | calldata, 36 | ); 37 | return `${functionName}(pid: ${this.formatPid( 38 | pid, 39 | )}, leftoverAmount: ${this.formatAmount( 40 | leftoverAmount, 41 | pid, 42 | )}, stake: ${stake})`; 43 | } 44 | 45 | case "withdraw": { 46 | const [pid, amount] = this.decodeFunctionData( 47 | functionFragment, 48 | calldata, 49 | ); 50 | return `${functionName}(pid: ${this.formatPid( 51 | pid, 52 | )}, amount: ${this.formatAmount(amount, pid)})`; 53 | } 54 | 55 | case "withdrawDiff": { 56 | const [pid, leftoverAmount] = this.decodeFunctionData( 57 | functionFragment, 58 | calldata, 59 | ); 60 | return `${functionName}(pid: ${this.formatPid( 61 | pid, 62 | )}, leftoverAmount: ${this.formatAmount(leftoverAmount, pid)})`; 63 | } 64 | 65 | default: 66 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 67 | } 68 | } 69 | 70 | formatAmount(amount: BigNumberish, pid: number): string { 71 | return this.formatBN(amount, convexLpTokenByPid[pid]); 72 | } 73 | 74 | formatPid(pid: number): string { 75 | return `${pid} [${convexPoolByPid[pid]}]`; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/convextRewardPoolParser.ts: -------------------------------------------------------------------------------- 1 | import type { NormalToken } from "@gearbox-protocol/sdk-gov"; 2 | import { IBaseRewardPool__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class ConvexRewardPoolParser extends AbstractParser implements IParser { 8 | constructor(token: NormalToken) { 9 | super(`ConvexRewardPool_${token}`); 10 | this.ifc = IBaseRewardPool__factory.createInterface(); 11 | } 12 | parse(calldata: string): string { 13 | const { functionFragment, functionName } = this.parseSelector(calldata); 14 | 15 | switch (functionFragment.name) { 16 | case "rewardRate": 17 | return `${functionName}()`; 18 | 19 | default: 20 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/creditFacadeParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedToken } from "@gearbox-protocol/sdk-gov"; 2 | import type { 3 | BalanceDeltaStructOutput, 4 | BalanceStructOutput, 5 | } from "@gearbox-protocol/types/v3"; 6 | import { ICreditFacadeV3Multicall__factory } from "@gearbox-protocol/types/v3"; 7 | import type { BigNumberish } from "ethers"; 8 | 9 | import { AbstractParser } from "./abstractParser.js"; 10 | import type { IParser } from "./iParser.js"; 11 | 12 | export class CreditFacadeParser extends AbstractParser implements IParser { 13 | version: number; 14 | 15 | constructor(token: SupportedToken, version: number) { 16 | super(token); 17 | this.version = version; 18 | this.ifc = ICreditFacadeV3Multicall__factory.createInterface(); 19 | 20 | this.adapterName = "CreditFacade"; 21 | } 22 | parse(calldata: string): string { 23 | const { functionFragment, functionName } = this.parseSelector(calldata); 24 | 25 | switch (functionFragment.name) { 26 | case "addCollateral": { 27 | const r = this.decodeFunctionData(functionFragment, calldata); 28 | 29 | const token = r[0]; 30 | const amount = r[1]; 31 | 32 | return `${functionName}(token: ${this.tokenSymbol( 33 | token, 34 | )}, amount: ${this.formatBN(amount, this.tokenSymbol(token))})`; 35 | } 36 | case "increaseDebt": 37 | case "decreaseDebt": { 38 | const [amount] = this.decodeFunctionData(functionFragment, calldata); 39 | return `${functionName}(amount: ${this.formatAmount(amount)})`; 40 | } 41 | case "enableToken": 42 | case "disableToken": { 43 | const [address] = this.decodeFunctionData(functionFragment, calldata); 44 | return `${functionName}(token: ${this.tokenSymbol(address)})`; 45 | } 46 | 47 | case "updateQuota": { 48 | const [address, quotaUpdate, minQuota] = this.decodeFunctionData( 49 | functionFragment, 50 | calldata, 51 | ); 52 | return `${functionName}(token: ${this.tokenSymbol( 53 | address, 54 | )}, quotaUpdate: ${this.formatAmount( 55 | quotaUpdate, 56 | )}, minQuota: ${this.formatAmount(minQuota)})`; 57 | } 58 | 59 | case "revertIfReceivedLessThan": { 60 | const [balances] = this.decodeFunctionData(functionFragment, calldata); 61 | 62 | const balancesStr = ( 63 | balances as Array 64 | ) 65 | .map(b => { 66 | const balance = "balance" in b ? b.balance : b.amount; 67 | const symbol = this.tokenSymbol(b.token); 68 | 69 | return `${symbol}: ${this.formatBN(balance, symbol)}`; 70 | }) 71 | .join(", "); 72 | 73 | return `${functionName}(${balancesStr})`; 74 | } 75 | 76 | case "withdrawCollateral": { 77 | const [token, amount, to] = this.decodeFunctionData( 78 | functionFragment, 79 | calldata, 80 | ); 81 | 82 | return `${functionName}(token: ${this.tokenSymbol( 83 | token, 84 | )}, withdraw: ${this.formatBN( 85 | amount, 86 | this.tokenSymbol(token), 87 | )}, to: ${to})`; 88 | } 89 | 90 | case "addCollateralWithPermit": { 91 | const [tokenAddress, amount, deadline, v, r, s] = 92 | this.decodeFunctionData(functionFragment, calldata); 93 | 94 | return `${functionName}(token: ${this.tokenSymbol( 95 | tokenAddress, 96 | )}, amount: ${this.formatBN( 97 | amount, 98 | this.tokenSymbol(tokenAddress), 99 | )}, ${[deadline, v, r, s].join(", ")})`; 100 | } 101 | 102 | case "compareBalances": { 103 | return `${functionName}()`; 104 | } 105 | 106 | case "setFullCheckParams": { 107 | const [collateralHints, minHealthFactor] = this.decodeFunctionData( 108 | functionFragment, 109 | calldata, 110 | ); 111 | 112 | return `${functionName}(token: ${collateralHints 113 | .map((a: BigNumberish) => this.formatAmount(a)) 114 | .join(", ")}, minHealthFactor: ${minHealthFactor})`; 115 | } 116 | 117 | case "storeExpectedBalances": { 118 | const [balanceDeltas] = this.decodeFunctionData( 119 | functionFragment, 120 | calldata, 121 | ); 122 | 123 | return `${functionName}(balanceDeltas: ${balanceDeltas 124 | .map( 125 | (b: BalanceDeltaStructOutput) => 126 | `${this.tokenSymbol(b.token)}: ${this.formatBN( 127 | b.amount, 128 | this.tokenSymbol(b.token), 129 | )}`, 130 | ) 131 | .join(", ")})`; 132 | } 133 | 134 | case "onDemandPriceUpdate": { 135 | const [token, reserve, data] = this.decodeFunctionData( 136 | functionFragment, 137 | calldata, 138 | ); 139 | 140 | return `${functionName}(token: ${this.tokenOrTickerSymbol( 141 | token, 142 | )}, reserve: ${reserve}, data: ${data})`; 143 | } 144 | 145 | default: 146 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 147 | } 148 | } 149 | 150 | formatAmount(amount: BigNumberish): string { 151 | return this.formatBN(amount, this.contract as SupportedToken); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/creditManagerParser.ts: -------------------------------------------------------------------------------- 1 | import { ICreditManagerV3__factory } from "@gearbox-protocol/types/v3"; 2 | 3 | import { AbstractParser } from "./abstractParser.js"; 4 | import type { IParser } from "./iParser.js"; 5 | 6 | export class CreditManagerParser extends AbstractParser implements IParser { 7 | constructor(version: number) { 8 | super(`CreditManager_V${version}`); 9 | this.ifc = ICreditManagerV3__factory.createInterface(); 10 | } 11 | parse(calldata: string): string { 12 | const { functionFragment, functionName } = this.parseSelector(calldata); 13 | 14 | switch (functionFragment.name) { 15 | case "creditConfigurator": { 16 | return `${functionName}()`; 17 | } 18 | 19 | default: 20 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/curveAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CurveParams, 3 | SupportedContract, 4 | SupportedToken, 5 | } from "@gearbox-protocol/sdk-gov"; 6 | import { contractParams, formatBN } from "@gearbox-protocol/sdk-gov"; 7 | import { 8 | ICurveV1_2AssetsAdapter__factory, 9 | ICurveV1_3AssetsAdapter__factory, 10 | ICurveV1_4AssetsAdapter__factory, 11 | } from "@gearbox-protocol/types/v3"; 12 | import type { BigNumberish } from "ethers"; 13 | 14 | import { AbstractParser } from "./abstractParser.js"; 15 | import type { IParser } from "./iParser.js"; 16 | 17 | export class CurveAdapterParser extends AbstractParser implements IParser { 18 | protected lpToken: SupportedToken; 19 | 20 | constructor(contract: SupportedContract, isContract: boolean) { 21 | super(contract); 22 | 23 | let contractName = ""; 24 | 25 | const nCoins = (contractParams[contract] as CurveParams).tokens.length; 26 | switch (nCoins) { 27 | case 2: 28 | this.ifc = ICurveV1_2AssetsAdapter__factory.createInterface(); 29 | contractName = `Curve2AssetsAdapter`; 30 | break; 31 | case 3: 32 | this.ifc = ICurveV1_3AssetsAdapter__factory.createInterface(); 33 | contractName = `Curve3AssetsAdapter`; 34 | break; 35 | case 4: 36 | this.ifc = ICurveV1_4AssetsAdapter__factory.createInterface(); 37 | contractName = `Curve4AssetsAdapter`; 38 | break; 39 | default: 40 | throw new Error(`Unsupported curve contract ${contract}`); 41 | } 42 | this.lpToken = (contractParams[contract] as CurveParams).lpToken; 43 | if (!isContract) this.adapterName = contractName; 44 | } 45 | parse(calldata: string): string { 46 | const { functionFragment, functionName } = this.parseSelector(calldata); 47 | 48 | switch (functionFragment.name) { 49 | case "exchange": 50 | case "exchange_underlying": { 51 | const [i, j, dx, min_dy] = this.decodeFunctionData( 52 | functionFragment, 53 | calldata, 54 | ); 55 | 56 | const iSym = 57 | functionFragment.name === "exchange_underlying" 58 | ? this.getUnderlyingTokenByIndex(i) 59 | : this.getTokenByIndex(i); 60 | 61 | const jSym = 62 | functionFragment.name === "exchange_underlying" 63 | ? this.getUnderlyingTokenByIndex(j) 64 | : this.getTokenByIndex(j); 65 | 66 | return `${functionName}(i ,j: ${iSym} => ${jSym}, dx: ${this.formatBN( 67 | dx, 68 | iSym, 69 | )}, min_dy: ${this.formatBN(min_dy, jSym)})`; 70 | } 71 | 72 | case "exchange_diff": 73 | case "exchange_diff_underlying": { 74 | const [i, j, leftoverAmount, rateMinRAY] = this.decodeFunctionData( 75 | functionFragment, 76 | calldata, 77 | ); 78 | 79 | const iSym = 80 | functionFragment.name === "exchange_diff_underlying" 81 | ? this.getUnderlyingTokenByIndex(i) 82 | : this.getTokenByIndex(i); 83 | 84 | const jSym = 85 | functionFragment.name === "exchange_diff_underlying" 86 | ? this.getUnderlyingTokenByIndex(j) 87 | : this.getTokenByIndex(j); 88 | 89 | return `${functionName}(i: ${iSym}, j: ${jSym}, leftoverAmount: ${this.formatBN( 90 | leftoverAmount, 91 | iSym, 92 | )}, rateMinRAY: ${formatBN(rateMinRAY, 27)}`; 93 | } 94 | 95 | case "add_liquidity_one_coin": { 96 | const [amount, i, minAmount] = this.decodeFunctionData( 97 | functionFragment, 98 | calldata, 99 | ); 100 | 101 | const iSym = this.getTokenByIndex(i); 102 | 103 | return `${functionName}(amount: ${this.formatBN( 104 | amount, 105 | iSym, 106 | )}, i: ${iSym}, minAmount: ${this.formatBN(minAmount, this.lpToken)})`; 107 | } 108 | 109 | case "add_diff_liquidity_one_coin": 110 | case "remove_diff_liquidity_one_coin": { 111 | const [leftoverAmount, i, rateMinRAY] = this.decodeFunctionData( 112 | functionFragment, 113 | calldata, 114 | ); 115 | 116 | return `${functionName}(leftoverAmount: ${this.formatBN( 117 | leftoverAmount, 118 | i, 119 | )}, i: ${this.getTokenByIndex(i)}, rateMinRAY: ${formatBN( 120 | rateMinRAY, 121 | 27, 122 | )})`; 123 | } 124 | 125 | case "add_liquidity": { 126 | const [amounts, minAmount] = this.decodeFunctionData( 127 | functionFragment, 128 | calldata, 129 | ); 130 | 131 | return `${functionName}(amounts: [${this.convertAmounts( 132 | amounts, 133 | )}], minAmount: ${this.formatBN(minAmount, this.lpToken)})`; 134 | } 135 | 136 | case "remove_liquidity": { 137 | const [amount, min_amounts] = this.decodeFunctionData( 138 | functionFragment, 139 | calldata, 140 | ); 141 | 142 | return `${functionName}(amount: ${this.formatBN( 143 | amount, 144 | this.lpToken, 145 | )}, min_amounts: [${this.convertAmounts(min_amounts)}])`; 146 | } 147 | 148 | case "remove_liquidity_imbalance": { 149 | const [amounts, maxBurnAmount] = this.decodeFunctionData( 150 | functionFragment, 151 | calldata, 152 | ); 153 | 154 | return `${functionName}(amounts: [${this.convertAmounts( 155 | amounts, 156 | )}], max_burn_amount: ${this.formatBN(maxBurnAmount, this.lpToken)})`; 157 | } 158 | 159 | case "remove_liquidity_one_coin": { 160 | const [amount, i, min_amount] = this.decodeFunctionData( 161 | functionFragment, 162 | calldata, 163 | ); 164 | 165 | const iSym = this.getTokenByIndex(i); 166 | 167 | return `${functionName}(amount: ${this.formatBN( 168 | amount, 169 | this.lpToken, 170 | )},i: ${iSym}, min_amount: ${this.formatBN(min_amount, iSym)})`; 171 | } 172 | 173 | case "totalSupply": { 174 | return `${functionName}()`; 175 | } 176 | 177 | case "balances": { 178 | const [i] = this.decodeFunctionData(functionFragment, calldata); 179 | return `${functionName}(${this.getTokenByIndex(i)})`; 180 | } 181 | case "balanceOf": { 182 | const [address] = this.decodeFunctionData(functionFragment, calldata); 183 | return `${functionName}(${address})`; 184 | } 185 | case "get_virtual_price": { 186 | return `${functionName}()`; 187 | } 188 | 189 | case "allowance": { 190 | const [account, to] = this.decodeFunctionData( 191 | functionFragment, 192 | calldata, 193 | ); 194 | return `${functionName}(account: ${account}, to: ${to})`; 195 | } 196 | 197 | default: 198 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 199 | } 200 | } 201 | 202 | getTokenByIndex(index: number): SupportedToken { 203 | return (contractParams[this.contract as SupportedContract] as CurveParams) 204 | .tokens[index]; 205 | } 206 | 207 | getUnderlyingTokenByIndex(index: number): SupportedToken { 208 | return (contractParams[this.contract as SupportedContract] as CurveParams) 209 | .underlyings![index]; 210 | } 211 | 212 | convertAmounts(amounts: Array): string { 213 | return amounts 214 | .map((a, coin) => { 215 | const sym = this.getTokenByIndex(coin); 216 | return `${sym}: ${this.formatBN(a, sym)}`; 217 | }) 218 | .join(", "); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/daiUsdsAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IDaiUsdsAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class DaiUsdsAdapterParser extends AbstractParser implements IParser { 8 | constructor(contract: SupportedContract, isContract: boolean) { 9 | super(contract); 10 | this.ifc = IDaiUsdsAdapter__factory.createInterface(); 11 | if (!isContract) this.adapterName = "DaiUsdsAdapter"; 12 | } 13 | 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | default: 19 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/erc626AdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IERC4626Adapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class ERC4626AdapterParser extends AbstractParser implements IParser { 8 | constructor(contract: SupportedContract, isContract: boolean) { 9 | super(contract); 10 | this.ifc = IERC4626Adapter__factory.createInterface(); 11 | if (!isContract) this.adapterName = "ERC4626Adapter"; 12 | } 13 | 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | default: 19 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/iParser.ts: -------------------------------------------------------------------------------- 1 | export interface IParser { 2 | parse: (calldata: string) => string; 3 | // parseToObject?: ( 4 | // address: string, 5 | // calldata: string, 6 | // ) => { 7 | // address: string; 8 | // functionFragment: FunctionFragment; 9 | // args: any; 10 | // }; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./txParser.js"; 2 | export * from "./TxParserHelper.js"; 3 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/lidoAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { ILidoV1Adapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class LidoAdapterParser extends AbstractParser implements IParser { 8 | constructor(contract: SupportedContract, isContract: boolean) { 9 | super(contract); 10 | this.ifc = ILidoV1Adapter__factory.createInterface(); 11 | if (!isContract) this.adapterName = "LidoV1Adapter"; 12 | } 13 | parse(calldata: string): string { 14 | const { functionFragment, functionName } = this.parseSelector(calldata); 15 | 16 | switch (functionFragment.name) { 17 | case "submit": { 18 | const [amount] = this.decodeFunctionData(functionFragment, calldata); 19 | return `${functionName}(amount: ${this.formatBN(amount, "STETH")})`; 20 | } 21 | case "submitDiff": { 22 | const [leftoverAmount] = this.decodeFunctionData( 23 | functionFragment, 24 | calldata, 25 | ); 26 | return `${functionName}(leftoverAmount: ${this.formatBN( 27 | leftoverAmount, 28 | "STETH", 29 | )})`; 30 | } 31 | 32 | default: 33 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/lidoSTETHParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedToken } from "@gearbox-protocol/sdk-gov"; 2 | import { toBigInt } from "@gearbox-protocol/sdk-gov"; 3 | import { IstETH__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class LidoSTETHParser extends AbstractParser implements IParser { 9 | constructor(symbol: SupportedToken) { 10 | super(`LIDO_${symbol}`); 11 | this.ifc = IstETH__factory.createInterface(); 12 | } 13 | 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | case "getFee": 19 | case "totalSupply": { 20 | return `${functionName}()`; 21 | } 22 | 23 | case "balanceOf": { 24 | const [address] = this.decodeFunctionData(functionFragment, calldata); 25 | 26 | return `${functionName}(${address})`; 27 | } 28 | 29 | case "allowance": { 30 | const [account, to] = this.decodeFunctionData( 31 | functionFragment, 32 | calldata, 33 | ); 34 | return `${functionName}(account: ${account}, to: ${to})`; 35 | } 36 | case "approve": { 37 | const [spender, amount] = this.decodeFunctionData( 38 | functionFragment, 39 | calldata, 40 | ); 41 | return `${functionName}(${spender}, [${toBigInt(amount).toString()}])`; 42 | } 43 | 44 | default: 45 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/poolParser.ts: -------------------------------------------------------------------------------- 1 | import { IPoolV3__factory } from "@gearbox-protocol/types/v3"; 2 | 3 | import { AbstractParser } from "./abstractParser.js"; 4 | import type { IParser } from "./iParser.js"; 5 | 6 | export class PoolParser extends AbstractParser implements IParser { 7 | constructor(version: number) { 8 | super(`Pool_V${version}`); 9 | this.ifc = IPoolV3__factory.createInterface(); 10 | } 11 | parse(calldata: string): string { 12 | const { functionFragment, functionName } = this.parseSelector(calldata); 13 | 14 | switch (functionFragment.name) { 15 | default: 16 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/priceOracleParser.ts: -------------------------------------------------------------------------------- 1 | import { IPriceOracleBase__factory } from "@gearbox-protocol/types/v3"; 2 | 3 | import { AbstractParser } from "./abstractParser.js"; 4 | import type { IParser } from "./iParser.js"; 5 | 6 | export class PriceOracleParser extends AbstractParser implements IParser { 7 | constructor() { 8 | super("PriceOracle"); 9 | this.ifc = IPriceOracleBase__factory.createInterface(); 10 | } 11 | parse(calldata: string): string { 12 | const { functionFragment, functionName } = this.parseSelector(calldata); 13 | 14 | switch (functionFragment.name) { 15 | case "getPrice": { 16 | const [token] = this.decodeFunctionData(functionFragment, calldata); 17 | 18 | return `${functionName}(${this.tokenSymbol(token)})`; 19 | } 20 | 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/stakingRewardsAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IStakingRewardsAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class StakingRewardsAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IStakingRewardsAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "StakingRewardsAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | const { functionFragment, functionName } = this.parseSelector(calldata); 19 | 20 | switch (functionFragment.name) { 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/uniV2AdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { formatBN } from "@gearbox-protocol/sdk-gov"; 3 | import { IUniswapV2Adapter__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class UniswapV2AdapterParser extends AbstractParser implements IParser { 9 | constructor(contract: SupportedContract, isContract: boolean) { 10 | super(contract); 11 | this.ifc = IUniswapV2Adapter__factory.createInterface(); 12 | if (!isContract) this.adapterName = "UniswapV2Adapter"; 13 | } 14 | parse(calldata: string): string { 15 | return this._parse(calldata); 16 | } 17 | 18 | protected _parse(calldata: string): string { 19 | const { functionFragment, functionName } = this.parseSelector(calldata); 20 | 21 | switch (functionFragment.name) { 22 | case "swapExactTokensForTokens": { 23 | const [amountIn, amountOutMin, path] = this.decodeFunctionData( 24 | functionFragment, 25 | calldata, 26 | ); 27 | const pathStr = (path as Array) 28 | .map(r => this.tokenSymbol(r)) 29 | .join(" => "); 30 | 31 | const tokenIn = this.tokenSymbol(path[0]); 32 | const tokenOut = this.tokenSymbol(path[path.length - 1]); 33 | const amountInStr = this.formatBN(amountIn, tokenIn); 34 | const amountOutStr = this.formatBN(amountOutMin, tokenOut); 35 | return `${functionName}(amountIn: ${amountInStr}, amountOutMin: ${amountOutStr}, path: [${pathStr}])`; 36 | } 37 | 38 | case "swapTokensForExactTokens": { 39 | const [amountOut, amountInMax, path] = this.decodeFunctionData( 40 | functionFragment, 41 | calldata, 42 | ); 43 | 44 | const pathStr = (path as Array) 45 | .map(r => this.tokenSymbol(r)) 46 | .join(" => "); 47 | 48 | const tokenIn = this.tokenSymbol(path[0]); 49 | const tokenOut = this.tokenSymbol(path[path.length - 1]); 50 | const amountOutStr = this.formatBN(amountOut, tokenIn); 51 | const amountInMaxStr = this.formatBN(amountInMax, tokenOut); 52 | 53 | return `${functionName}(amountOut: ${amountOutStr}, amountInMax: ${amountInMaxStr}, path: [${pathStr}])`; 54 | } 55 | 56 | case "swapDiffTokensForTokens": { 57 | const [leftoverAmount, rateMinRAY, path] = this.decodeFunctionData( 58 | functionFragment, 59 | calldata, 60 | ); 61 | 62 | const tokenIn = this.tokenSymbol(path[0]); 63 | 64 | return `${functionName}(leftoverAmount: ${this.formatBN( 65 | leftoverAmount, 66 | tokenIn, 67 | )}, rate: ${formatBN(rateMinRAY, 27)}, path: [${(path as Array) 68 | .map(r => this.tokenSymbol(r)) 69 | .join(" => ")}])`; 70 | } 71 | 72 | default: 73 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/uniV3AdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { formatBN } from "@gearbox-protocol/sdk-gov"; 3 | import { IUniswapV3Adapter__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class UniswapV3AdapterParser extends AbstractParser implements IParser { 9 | constructor(contract: SupportedContract, isContract: boolean) { 10 | super(contract); 11 | this.ifc = IUniswapV3Adapter__factory.createInterface(); 12 | if (!isContract) this.adapterName = "UniswapV3Adapter"; 13 | } 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | case "exactInputSingle": { 19 | const [[tokenIn, tokenOut, fee, , , amountIn, amountOutMinimum]] = 20 | this.decodeFunctionData(functionFragment, calldata); 21 | const tokenInSym = this.tokenSymbol(tokenIn); 22 | const tokenOutSym = this.tokenSymbol(tokenOut); 23 | 24 | const amountInStr = this.formatBN(amountIn, tokenIn); 25 | const amountOutMinimumStr = this.formatBN(amountOutMinimum, tokenOut); 26 | 27 | return `${functionName}(amountIn: ${amountInStr}, amountOutMinimum: ${amountOutMinimumStr}, path: ${tokenInSym} ==(fee: ${fee})==> ${tokenOutSym})`; 28 | } 29 | case "exactDiffInputSingle": { 30 | const { 31 | params: { tokenIn, tokenOut, fee, leftoverAmount, rateMinRAY }, 32 | } = this.decodeFunctionData(functionFragment, calldata); 33 | const tokenInSym = this.tokenSymbol(tokenIn); 34 | const tokenOutSym = this.tokenSymbol(tokenOut); 35 | 36 | const leftoverAmountStr = this.formatBN(leftoverAmount, tokenIn); 37 | return `${functionName}(leftoverAmount: ${leftoverAmountStr}, rate: ${formatBN( 38 | rateMinRAY, 39 | 27, 40 | )}, path: ${tokenInSym} ==(fee: ${fee})==> ${tokenOutSym})`; 41 | } 42 | case "exactInput": { 43 | const { 44 | params: { amountIn, amountOutMinimum, path }, 45 | } = this.decodeFunctionData(functionFragment, calldata); 46 | 47 | const pathStr = this.trackInputPath(path); 48 | const amountInStr = this.formatBN( 49 | amountIn, 50 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 51 | ); 52 | 53 | const amountOutMinimumStr = this.formatBN( 54 | amountOutMinimum, 55 | this.tokenSymbol(`0x${path.slice(-40, path.length)}`), 56 | ); 57 | 58 | return `${functionName}(amountIn: ${amountInStr}, amountOutMinimum: ${amountOutMinimumStr}, path: ${pathStr}`; 59 | } 60 | case "exactDiffInput": { 61 | const { 62 | params: { leftoverAmount, rateMinRAY, path }, 63 | } = this.decodeFunctionData(functionFragment, calldata); 64 | 65 | const leftoverAmountStr = this.formatBN( 66 | leftoverAmount, 67 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 68 | ); 69 | 70 | const pathStr = this.trackInputPath(path); 71 | 72 | return `${functionName}(leftoverAmount: ${leftoverAmountStr}, rate: ${formatBN( 73 | rateMinRAY, 74 | 27, 75 | )}, path: ${pathStr}`; 76 | } 77 | case "exactOutput": { 78 | const { 79 | params: { amountInMaximum, amountOut, path }, 80 | } = this.decodeFunctionData(functionFragment, calldata); 81 | 82 | const pathStr = this.trackOutputPath(path); 83 | const amountInMaximumStr = this.formatBN( 84 | amountInMaximum, 85 | this.tokenSymbol(`0x${path.slice(-40, path.length)}`), 86 | ); 87 | 88 | const amountOutStr = this.formatBN( 89 | amountOut, 90 | this.tokenSymbol(`0x${path.replace("0x", "").slice(0, 40)}`), 91 | ); 92 | 93 | return `${functionName}(amountInMaximum: ${amountInMaximumStr}, amountOut: ${amountOutStr}, path: ${pathStr}`; 94 | } 95 | case "exactOutputSingle": { 96 | const [[tokenIn, tokenOut, fee, , , amountOut, amountInMaximum]] = 97 | this.decodeFunctionData(functionFragment, calldata); 98 | 99 | const tokenInSym = this.tokenSymbol(tokenIn); 100 | const tokenOutSym = this.tokenSymbol(tokenOut); 101 | 102 | const amountInMaximumStr = this.formatBN(amountInMaximum, tokenInSym); 103 | const amountOutStr = this.formatBN(amountOut, tokenOutSym); 104 | 105 | return `${functionName}(amountInMaximum: ${amountInMaximumStr}, amountOut: ${amountOutStr}, path: ${tokenInSym} ==(fee: ${fee})==> ${tokenOutSym})`; 106 | } 107 | 108 | default: 109 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 110 | } 111 | } 112 | 113 | trackInputPath(path: string): string { 114 | let result = ""; 115 | let pointer = path.startsWith("0x") ? 2 : 0; 116 | while (pointer <= path.length - 40) { 117 | const from = `0x${path.slice(pointer, pointer + 40)}`.toLowerCase(); 118 | result += this.tokenSymbol(from) || from; 119 | pointer += 40; 120 | 121 | if (pointer > path.length - 6) return result; 122 | 123 | const fee = parseInt(path.slice(pointer, pointer + 6), 16); 124 | 125 | pointer += 6; 126 | result += ` ==(fee: ${fee})==> `; 127 | } 128 | 129 | return result; 130 | } 131 | 132 | trackOutputPath(path: string): string { 133 | let result = ""; 134 | let pointer = path.length; 135 | while (pointer >= 40) { 136 | pointer -= 40; 137 | const from = `0x${path.slice(pointer, pointer + 40)}`; 138 | result += this.tokenSymbol(from) || from; 139 | 140 | if (pointer < 6) return result; 141 | pointer -= 6; 142 | const fee = parseInt(path.slice(pointer, pointer + 6), 16); 143 | 144 | result += ` ==(fee: ${fee})==> `; 145 | } 146 | 147 | return result; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/velodromeV2RouterAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { IVelodromeV2RouterAdapter__factory } from "@gearbox-protocol/types/v3"; 3 | 4 | import { AbstractParser } from "./abstractParser.js"; 5 | import type { IParser } from "./iParser.js"; 6 | 7 | export class VelodromeV2RouterAdapterParser 8 | extends AbstractParser 9 | implements IParser 10 | { 11 | constructor(contract: SupportedContract, isContract: boolean) { 12 | super(contract); 13 | this.ifc = IVelodromeV2RouterAdapter__factory.createInterface(); 14 | if (!isContract) this.adapterName = "VelodromeV2RouterAdapter"; 15 | } 16 | 17 | parse(calldata: string): string { 18 | const { functionFragment, functionName } = this.parseSelector(calldata); 19 | 20 | switch (functionFragment.name) { 21 | default: 22 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/wstETHAdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { toBigInt } from "@gearbox-protocol/sdk-gov"; 3 | import { 4 | IwstETH__factory, 5 | IwstETHV1Adapter__factory, 6 | } from "@gearbox-protocol/types/v3"; 7 | 8 | import { AbstractParser } from "./abstractParser.js"; 9 | import type { IParser } from "./iParser.js"; 10 | 11 | export class WstETHAdapterParser extends AbstractParser implements IParser { 12 | constructor(contract: SupportedContract, isContract: boolean) { 13 | super(contract); 14 | this.ifc = !isContract 15 | ? IwstETHV1Adapter__factory.createInterface() 16 | : IwstETH__factory.createInterface(); 17 | if (!isContract) this.adapterName = "wstETHAdapter"; 18 | } 19 | parse(calldata: string): string { 20 | const { functionFragment, functionName } = this.parseSelector(calldata); 21 | 22 | switch (functionFragment.name) { 23 | case "wrap": { 24 | const [amount] = this.decodeFunctionData(functionFragment, calldata); 25 | return `${functionName}(amount: ${this.formatBN(amount, "STETH")})`; 26 | } 27 | case "wrapDiff": { 28 | const [leftoverAmount] = this.decodeFunctionData( 29 | functionFragment, 30 | calldata, 31 | ); 32 | return `${functionName}(leftoverAmount: ${this.formatBN( 33 | leftoverAmount, 34 | "STETH", 35 | )})`; 36 | } 37 | 38 | case "unwrap": { 39 | const [amount] = this.decodeFunctionData(functionFragment, calldata); 40 | return `${functionName}(amount: ${this.formatBN(amount, "wstETH")})`; 41 | } 42 | case "unwrapDiff": { 43 | const [leftoverAmount] = this.decodeFunctionData( 44 | functionFragment, 45 | calldata, 46 | ); 47 | return `${functionName}(leftoverAmount: ${this.formatBN( 48 | leftoverAmount, 49 | "STETH", 50 | )})`; 51 | } 52 | 53 | case "balanceOf": { 54 | const [address] = this.decodeFunctionData(functionFragment, calldata); 55 | return `${functionName}(${address})`; 56 | } 57 | case "allowance": { 58 | const [account, to] = this.decodeFunctionData( 59 | functionFragment, 60 | calldata, 61 | ); 62 | return `${functionName}(account: ${account}, to: ${to})`; 63 | } 64 | case "approve": { 65 | const [spender, amount] = this.decodeFunctionData( 66 | functionFragment, 67 | calldata, 68 | ); 69 | return `${functionName}(${spender}, [${toBigInt(amount).toString()}])`; 70 | } 71 | 72 | default: 73 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/ethers-6-temp/txparser/yearnV2AdapterParser.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedContract } from "@gearbox-protocol/sdk-gov"; 2 | import { contractsByNetwork } from "@gearbox-protocol/sdk-gov"; 3 | import { IYearnV2Adapter__factory } from "@gearbox-protocol/types/v3"; 4 | 5 | import { AbstractParser } from "./abstractParser.js"; 6 | import type { IParser } from "./iParser.js"; 7 | 8 | export class YearnV2AdapterParser extends AbstractParser implements IParser { 9 | constructor(contract: SupportedContract, isContract: boolean) { 10 | super(contract); 11 | this.ifc = IYearnV2Adapter__factory.createInterface(); 12 | if (!isContract) this.adapterName = "YearnV2Adapter"; 13 | } 14 | parse(calldata: string): string { 15 | const { functionFragment, functionName } = this.parseSelector(calldata); 16 | 17 | switch (functionFragment.name) { 18 | case "deposit": 19 | case "withdraw": 20 | case "withdraw(uint256,address,uint256)": { 21 | const [amount, address, maxLoss] = this.decodeFunctionData( 22 | functionFragment, 23 | calldata, 24 | ); 25 | 26 | const yvSym = this.tokenSymbol( 27 | contractsByNetwork.Mainnet[this.contract as SupportedContract], 28 | ); 29 | 30 | const amountStr = amount 31 | ? `amount: ${this.formatBN(amount, yvSym)}` 32 | : ""; 33 | const addressStr = address ? `, address: ${address}` : ""; 34 | const maxLossStr = maxLoss ? `, maxLoss: ${maxLoss}` : ""; 35 | 36 | return `${functionName}(${amountStr}${addressStr}${maxLossStr})`; 37 | } 38 | 39 | case "depositDiff": { 40 | const [leftoverAmount] = this.decodeFunctionData( 41 | functionFragment, 42 | calldata, 43 | ); 44 | 45 | const yvSym = this.tokenSymbol( 46 | contractsByNetwork.Mainnet[this.contract as SupportedContract], 47 | ); 48 | 49 | const leftoverAmountStr = this.formatBN(leftoverAmount, yvSym); 50 | 51 | return `${functionName}(leftoverAmount: ${leftoverAmountStr})`; 52 | } 53 | 54 | case "withdrawDiff": { 55 | const [leftoverAmount] = this.decodeFunctionData( 56 | functionFragment, 57 | calldata, 58 | ); 59 | 60 | const yvSym = this.tokenSymbol( 61 | contractsByNetwork.Mainnet[this.contract as SupportedContract], 62 | ); 63 | 64 | const leftoverAmountStr = this.formatBN(leftoverAmount, yvSym); 65 | 66 | return `${functionName}(leftoverAmount: ${leftoverAmountStr})`; 67 | } 68 | 69 | case "pricePerShare": { 70 | return `${functionName}()`; 71 | } 72 | case "balanceOf": { 73 | const [address] = this.decodeFunctionData(functionFragment, calldata); 74 | return `${functionName}(${address})`; 75 | } 76 | 77 | case "allowance": { 78 | const [account, to] = this.decodeFunctionData( 79 | functionFragment, 80 | calldata, 81 | ); 82 | return `${functionName}(account: ${account}, to: ${to})`; 83 | } 84 | 85 | default: 86 | return `${functionName}: Unknown operation ${functionFragment.name} with calldata ${calldata}`; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/etherscan.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkType } from "@gearbox-protocol/sdk-gov"; 2 | import type { TransactionReceipt } from "viem"; 3 | 4 | import type { CreditAccountData } from "../data/index.js"; 5 | 6 | export type EtherscanURLParam = 7 | | { block: number } 8 | | { tx: string } 9 | | { address: string }; 10 | 11 | export function etherscanUrl( 12 | entity: EtherscanURLParam | TransactionReceipt | CreditAccountData, 13 | network: NetworkType, 14 | ): string { 15 | let [prefix, domain] = ["", "etherscan.io"]; 16 | 17 | let param: EtherscanURLParam; 18 | if ("transactionHash" in entity && "blockHash" in entity) { 19 | param = { tx: entity.transactionHash }; 20 | } else if ("addr" in entity && "creditManager" in entity) { 21 | param = { address: entity.addr }; 22 | } else { 23 | param = entity; 24 | } 25 | 26 | switch (network) { 27 | case "Optimism": 28 | prefix = "optimistic."; 29 | break; 30 | case "Arbitrum": 31 | domain = "arbiscan.io"; 32 | break; 33 | case "Base": 34 | domain = "basescan.org"; 35 | break; 36 | } 37 | const [key, value] = Object.entries(param)[0]; 38 | return `https://${prefix}${domain}/${key}/${value}`; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | /** 4 | * Formats block timestamp or something that contains it 5 | * @param t 6 | * @returns 7 | */ 8 | export function formatTs( 9 | t: 10 | | number 11 | | bigint 12 | | string 13 | | { timestamp: number | bigint | string } 14 | | { ts: number | bigint | string } 15 | | null 16 | | undefined, 17 | ): string { 18 | if (!t) { 19 | return "null"; 20 | } 21 | const ts = typeof t === "object" ? ("ts" in t ? t.ts : t.timestamp) : t; 22 | const d = new Date(Number(ts) * 1000); 23 | return `${format(d, "dd/MM/yy HH:mm:ss")} (${ts})`; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/getLogsPaginated.ts: -------------------------------------------------------------------------------- 1 | import type { AbiEvent } from "abitype"; 2 | import type { 3 | BlockNumber, 4 | Chain, 5 | Client, 6 | GetLogsParameters, 7 | GetLogsReturnType, 8 | MaybeAbiEventName, 9 | Transport, 10 | } from "viem"; 11 | import { getLogs } from "viem/actions"; 12 | 13 | export type GetLogsPaginatedParameters< 14 | abiEvent extends AbiEvent | undefined = undefined, 15 | abiEvents extends 16 | | readonly AbiEvent[] 17 | | readonly unknown[] 18 | | undefined = abiEvent extends AbiEvent ? [abiEvent] : undefined, 19 | strict extends boolean | undefined = undefined, 20 | // 21 | _eventName extends string | undefined = MaybeAbiEventName, 22 | > = GetLogsParameters< 23 | abiEvent, 24 | abiEvents, 25 | strict, 26 | BlockNumber, 27 | BlockNumber, 28 | _eventName 29 | > & { 30 | pageSize: bigint; 31 | }; 32 | 33 | /** 34 | * Get logs in pages, to avoid rate limiting 35 | * Must be used with client that has batching enabled 36 | * @param client 37 | * @param params 38 | * @returns 39 | */ 40 | export async function getLogsPaginated< 41 | chain extends Chain | undefined, 42 | const abiEvent extends AbiEvent | undefined = undefined, 43 | const abiEvents extends 44 | | readonly AbiEvent[] 45 | | readonly unknown[] 46 | | undefined = abiEvent extends AbiEvent ? [abiEvent] : undefined, 47 | strict extends boolean | undefined = undefined, 48 | >( 49 | client: Client, 50 | params: GetLogsPaginatedParameters, 51 | ): Promise< 52 | GetLogsReturnType 53 | > { 54 | const from_ = params.fromBlock as bigint; 55 | const to_ = params.toBlock as bigint; 56 | const pageSize = params.pageSize; 57 | const requests: GetLogsParameters< 58 | abiEvent, 59 | abiEvents, 60 | strict, 61 | BlockNumber, 62 | BlockNumber 63 | >[] = []; 64 | for (let fromBlock = from_; fromBlock < to_; fromBlock += pageSize) { 65 | let toBlock = fromBlock + pageSize - 1n; 66 | if (toBlock > to_) { 67 | toBlock = to_; 68 | } 69 | requests.push({ 70 | ...params, 71 | fromBlock, 72 | toBlock, 73 | } as GetLogsParameters< 74 | abiEvent, 75 | abiEvents, 76 | strict, 77 | BlockNumber, 78 | BlockNumber 79 | >); 80 | } 81 | const responses = await Promise.all( 82 | requests.map(r => 83 | getLogs( 84 | client, 85 | r, 86 | ), 87 | ), 88 | ); 89 | 90 | return responses.flat().sort((a, b) => { 91 | if (a.blockNumber === b.blockNumber) { 92 | return a.logIndex - b.logIndex; 93 | } else if (a.blockNumber < b.blockNumber) { 94 | return -1; 95 | } else { 96 | return 1; 97 | } 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bigint-serializer.js"; 2 | export * from "./bigint-utils.js"; 3 | export * from "./detect-network.js"; 4 | export * from "./etherscan.js"; 5 | export * from "./formatters.js"; 6 | export * from "./getLogsPaginated.js"; 7 | export * from "./retry.js"; 8 | export * from "./simulateMulticall.js"; 9 | export * from "./types.js"; 10 | export * from "./typescript.js"; 11 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout } from "node:timers/promises"; 2 | 3 | export interface RetryOptions { 4 | attempts?: number; 5 | interval?: number; 6 | } 7 | 8 | export async function retry( 9 | fn: () => Promise, 10 | options: RetryOptions = {}, 11 | ): Promise { 12 | const { attempts = 3, interval = 200 } = options; 13 | let cause: any; 14 | for (let i = 0; i < attempts; i++) { 15 | try { 16 | const result = await fn(); 17 | return result; 18 | } catch (e) { 19 | cause = e; 20 | await setTimeout(interval); 21 | } 22 | } 23 | throw new Error(`all attempts failed: ${cause}`); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { iDataCompressorV3Abi } from "@gearbox-protocol/types/abi"; 2 | import type { GetContractReturnType, PublicClient } from "viem"; 3 | 4 | export type Numberish = number | bigint; 5 | export type Arrayish = readonly T[] | T[]; 6 | 7 | export type ArrayElementType = T extends readonly (infer U)[] | (infer U)[] 8 | ? U 9 | : never; 10 | 11 | export type IDataCompressorContract = GetContractReturnType< 12 | typeof iDataCompressorV3Abi, 13 | PublicClient 14 | >; 15 | -------------------------------------------------------------------------------- /src/utils/typescript.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Typescript helper for converting json interfaces to classes 3 | * @returns 4 | */ 5 | export function createClassFromType() { 6 | return class { 7 | constructor(args: T) { 8 | Object.assign(this, args); 9 | } 10 | } as new (args: T) => T; 11 | } 12 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | const version = 2 | // set in docker build 3 | process.env.PACKAGE_VERSION ?? "dev"; 4 | 5 | export default version; 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "lib": ["esnext"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "outDir": "./build", 11 | "strict": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------