diff --git a/package.json b/package.json index 489a330..343d458 100644 --- a/package.json +++ b/package.json @@ -23,21 +23,21 @@ }, "devDependencies": { "@aws-sdk/client-s3": "^3.637.0", - "@commitlint/cli": "^19.4.0", - "@commitlint/config-conventional": "^19.2.2", + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", "@flashbots/ethers-provider-bundle": "^1.0.0", "@gearbox-protocol/eslint-config": "2.0.0-next.2", - "@gearbox-protocol/liquidator-v2-contracts": "^2.1.0", + "@gearbox-protocol/liquidator-v2-contracts": "^2.2.1", "@gearbox-protocol/prettier-config": "2.0.0-next.0", - "@gearbox-protocol/sdk-gov": "^2.18.5", + "@gearbox-protocol/sdk-gov": "^2.20.0", "@gearbox-protocol/types": "^1.12.1", "@redstone-finance/evm-connector": "^0.6.1", - "@types/node": "^22.5.0", + "@types/node": "^22.5.2", "@uniswap/sdk-core": "^5.3.1", "@uniswap/v3-sdk": "^3.13.1", "@vlad-yakovlev/telegram-md": "^2.0.0", "abitype": "^1.0.6", - "axios": "^1.7.5", + "axios": "^1.7.7", "axios-retry": "^4.5.0", "date-fns": "^3.6.0", "di-at-home": "^0.0.7", @@ -46,7 +46,7 @@ "eslint": "^8.57.0", "ethers": "^6.13.2", "husky": "^9.1.5", - "lint-staged": "^15.2.9", + "lint-staged": "^15.2.10", "nanoid": "^5.0.7", "node-pty": "^1.0.0", "pino": "^9.3.2", @@ -54,7 +54,7 @@ "redstone-protocol": "^1.0.5", "tsx": "^4.19.0", "typescript": "^5.5.4", - "viem": "^2.20.1", + "viem": "^2.21.1", "vitest": "^2.0.5" }, "prettier": "@gearbox-protocol/prettier-config", diff --git a/src/app.ts b/src/app.ts index 09433bb..fc547a7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,4 @@ -import type { Config } from "./config/index.js"; -import { loadConfig } from "./config/index.js"; +import { Config } from "./config/index.js"; import { DI } from "./di.js"; import { type ILogger, Logger } from "./log/index.js"; import type { AddressProviderService } from "./services/AddressProviderService.js"; @@ -68,7 +67,7 @@ class App { } export async function launchApp(): Promise { - const config = await loadConfig(); + const config = await Config.load(); DI.set(DI.Config, config); const app = new App(); await app.launch(); diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..c362042 --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,52 @@ +import type { NetworkType } from "@gearbox-protocol/sdk-gov"; +import { createPublicClient, http } from "viem"; + +import { createClassFromType, detectNetwork } from "../utils/index.js"; +import { envConfig } from "./env.js"; +import { ConfigSchema } from "./schema.js"; + +interface DynamicConfig { + readonly network: NetworkType; + readonly chainId: number; + readonly startBlock: bigint; +} + +const ConfigClass = createClassFromType(); + +export class Config extends ConfigClass { + static async load(): Promise { + const schema = ConfigSchema.parse(envConfig); + + const client = createPublicClient({ + transport: http(schema.ethProviderRpcs[0]), + name: "detect network client", + }); + + const [startBlock, chainId, network] = await Promise.all([ + client.getBlockNumber(), + client.getChainId(), + detectNetwork(client), + ]); + return new Config({ + ...schema, + startBlock, + chainId: Number(chainId), + network, + }); + } + + public get isPartial(): boolean { + return !!( + this.aavePartialLiquidatorAddress || + this.ghoPartialLiquidatorAddress || + this.deployAaveLiquidatorContracts || + this.deployGhoLiquidatorContracts + ); + } + + public get isBatch(): boolean { + return !!( + this.deployBatchLiquidatorContracts || this.batchLiquidatorAddress + ); + } +} diff --git a/src/config/env.ts b/src/config/env.ts index 1c84e14..595d130 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -8,7 +8,8 @@ const envConfigMapping: Record = { debugManagers: "DEBUG_MANAGERS", batchSize: "BATCH_SIZE", castBin: "CAST_BIN", - deployPartialLiquidatorContracts: "DEPLOY_PARTIAL_LIQUIDATOR", + deployAaveLiquidatorContracts: "DEPLOY_AAVE_PARTIAL_LIQUIDATOR", + deployGhoLiquidatorContracts: "DEPLOY_GHO_PARTIAL_LIQUIDATOR", deployBatchLiquidatorContracts: "DEPLOY_BATCH_LIQUIDATOR", ethProviderRpcs: ["JSON_RPC_PROVIDERS", "JSON_RPC_PROVIDER"], hfThreshold: "HF_TRESHOLD", @@ -22,7 +23,8 @@ const envConfigMapping: Record = { outFileName: "OUT_FILE_NAME", outS3Bucket: "OUT_S3_BUCKET", outS3Prefix: "OUT_S3_PREFIX", - partialLiquidatorAddress: "PARTIAL_LIQUIDATOR_ADDRESS", + aavePartialLiquidatorAddress: "AAVE_PARTIAL_LIQUIDATOR_ADDRESS", + ghoPartialLiquidatorAddress: "GHO_PARTIAL_LIQUIDATOR_ADDRESS", privateKey: "PRIVATE_KEY", port: "PORT", slippage: "SLIPPAGE", diff --git a/src/config/index.ts b/src/config/index.ts index 61fc1f4..1ccc32a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,29 +1 @@ -import { createPublicClient, http } from "viem"; - -import { detectNetwork } from "../utils/index.js"; -import { envConfig } from "./env.js"; -import type { Config } from "./schema.js"; -import { ConfigSchema } from "./schema.js"; - -export async function loadConfig(): Promise { - const schema = ConfigSchema.parse(envConfig); - - const client = createPublicClient({ - transport: http(schema.ethProviderRpcs[0]), - name: "detect network client", - }); - - const [startBlock, chainId, network] = await Promise.all([ - client.getBlockNumber(), - client.getChainId(), - detectNetwork(client), - ]); - return { - ...schema, - startBlock, - chainId: Number(chainId), - network, - }; -} - -export type { Config } from "./schema.js"; +export { Config } from "./config.js"; diff --git a/src/config/schema.ts b/src/config/schema.ts index d88e405..03368d7 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -1,4 +1,4 @@ -import { MAX_INT, type NetworkType } from "@gearbox-protocol/sdk-gov"; +import { MAX_INT } from "@gearbox-protocol/sdk-gov"; import { Address } from "abitype/zod"; import { type Hex, isHex } from "viem"; import { z } from "zod"; @@ -48,8 +48,10 @@ export const ConfigSchema = z.object({ */ hfThreshold: z.coerce.bigint().min(0n).max(MAX_INT).default(MAX_INT), optimistic: booleanLike.pipe(z.boolean().optional()), - deployPartialLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), - partialLiquidatorAddress: Address.optional(), + deployAaveLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), + aavePartialLiquidatorAddress: Address.optional(), + deployGhoLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), + ghoPartialLiquidatorAddress: Address.optional(), deployBatchLiquidatorContracts: booleanLike.pipe(z.boolean().optional()), batchSize: z.coerce.number().nonnegative().default(10), batchLiquidatorAddress: Address.optional(), @@ -72,12 +74,3 @@ export const ConfigSchema = z.object({ }); export type ConfigSchema = z.infer; - -/** - * Config + derived fields - */ -export type Config = ConfigSchema & { - network: NetworkType; - chainId: number; - startBlock: bigint; -}; diff --git a/src/services/liquidate/AAVELiquidatorContract.ts b/src/services/liquidate/AAVELiquidatorContract.ts new file mode 100644 index 0000000..97e7070 --- /dev/null +++ b/src/services/liquidate/AAVELiquidatorContract.ts @@ -0,0 +1,100 @@ +import { + aaveFlTakerAbi, + aaveLiquidatorAbi, +} from "@gearbox-protocol/liquidator-v2-contracts/abi"; +import { + AaveFLTaker_bytecode, + AaveLiquidator_bytecode, +} from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; +import { contractsByNetwork } from "@gearbox-protocol/sdk-gov"; +import type { Address } from "viem"; + +import type { ILogger } from "../../log/index.js"; +import { Logger } from "../../log/index.js"; +import PartialLiquidatorContract from "./PartialLiquidatorContract.js"; + +export default class AAVELiquidatorContract extends PartialLiquidatorContract { + @Logger("AAVEPartialLiquidator") + logger!: ILogger; + + constructor(router: Address, bot: Address) { + super("AAVE Partial Liquidator", router, bot); + } + + public async deploy(): Promise { + let address = this.config.aavePartialLiquidatorAddress; + const aavePool = + contractsByNetwork[this.config.network].AAVE_V3_LENDING_POOL; + if (!address) { + this.logger.debug( + { aavePool, router: this.router, bot: this.bot }, + "deploying partial liquidator", + ); + + let hash = await this.client.wallet.deployContract({ + abi: aaveFlTakerAbi, + bytecode: AaveFLTaker_bytecode, + args: [aavePool], + }); + this.logger.debug(`waiting for AaveFLTaker to deploy, tx hash: ${hash}`); + const { contractAddress: aaveFlTakerAddr } = + await this.client.pub.waitForTransactionReceipt({ + hash, + timeout: 120_000, + }); + if (!aaveFlTakerAddr) { + throw new Error(`AaveFLTaker was not deployed, tx hash: ${hash}`); + } + let owner = await this.client.pub.readContract({ + abi: aaveFlTakerAbi, + functionName: "owner", + address: aaveFlTakerAddr, + }); + this.logger.debug( + `deployed AaveFLTaker at ${aaveFlTakerAddr} owned by ${owner} in tx ${hash}`, + ); + + hash = await this.client.wallet.deployContract({ + abi: aaveLiquidatorAbi, + bytecode: AaveLiquidator_bytecode, + args: [this.router, this.bot, aavePool, aaveFlTakerAddr], + }); + this.logger.debug(`waiting for liquidator to deploy, tx hash: ${hash}`); + const { contractAddress: liquidatorAddr } = + await this.client.pub.waitForTransactionReceipt({ + hash, + timeout: 120_000, + }); + if (!liquidatorAddr) { + throw new Error(`liquidator was not deployed, tx hash: ${hash}`); + } + owner = await this.client.pub.readContract({ + abi: aaveLiquidatorAbi, + address: liquidatorAddr, + functionName: "owner", + }); + this.logger.debug( + `deployed Liquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, + ); + + const receipt = await this.client.simulateAndWrite({ + address: aaveFlTakerAddr, + abi: aaveFlTakerAbi, + functionName: "setAllowedFLReceiver", + args: [liquidatorAddr, true], + }); + if (receipt.status === "reverted") { + throw new Error( + `AaveFLTaker.setAllowedFLReceiver reverted, tx hash: ${receipt.transactionHash}`, + ); + } + this.logger.debug( + `set allowed flashloan receiver on FLTaker ${aaveFlTakerAddr} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, + ); + + address = liquidatorAddr; + } + this.logger.info(`partial liquidator contract addesss: ${address}`); + this.address = address; + } +} diff --git a/src/services/liquidate/GHOLiquidatorContract.ts b/src/services/liquidate/GHOLiquidatorContract.ts new file mode 100644 index 0000000..04b5e5e --- /dev/null +++ b/src/services/liquidate/GHOLiquidatorContract.ts @@ -0,0 +1,122 @@ +import { + ghoFmTakerAbi, + ghoLiquidatorAbi, +} from "@gearbox-protocol/liquidator-v2-contracts/abi"; +import { + GhoFMTaker_bytecode, + GhoLiquidator_bytecode, +} from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; +import { tokenDataByNetwork } from "@gearbox-protocol/sdk-gov"; +import type { Address } from "viem"; + +import type { ILogger } from "../../log/index.js"; +import { Logger } from "../../log/index.js"; +import PartialLiquidatorContract from "./PartialLiquidatorContract.js"; + +export default class GHOLiquidatorContract extends PartialLiquidatorContract { + @Logger("GHOPartialLiquidator") + logger!: ILogger; + + constructor(router: Address, bot: Address) { + super("GHO Partial Liquidator", router, bot); + } + + public async deploy(): Promise { + let address = this.config.ghoPartialLiquidatorAddress; + if (!address) { + this.logger.debug( + { + ghoFlashMinter: this.ghoFlashMinter, + router: this.router, + bot: this.bot, + }, + "deploying partial liquidator", + ); + + let hash = await this.client.wallet.deployContract({ + abi: ghoFmTakerAbi, + bytecode: GhoFMTaker_bytecode, + // constructor(address _ghoFlashMinter, address _gho) { + args: [ + this.ghoFlashMinter, + tokenDataByNetwork[this.config.network].GHO, + ], + }); + this.logger.debug(`waiting for GhoFMTaker to deploy, tx hash: ${hash}`); + const { contractAddress: ghoFMTakerAddr } = + await this.client.pub.waitForTransactionReceipt({ + hash, + timeout: 120_000, + }); + if (!ghoFMTakerAddr) { + throw new Error(`GhoFMTaker was not deployed, tx hash: ${hash}`); + } + let owner = await this.client.pub.readContract({ + abi: ghoFmTakerAbi, + functionName: "owner", + address: ghoFMTakerAddr, + }); + this.logger.debug( + `deployed GhoFMTaker at ${ghoFMTakerAddr} owned by ${owner} in tx ${hash}`, + ); + + hash = await this.client.wallet.deployContract({ + abi: ghoLiquidatorAbi, + bytecode: GhoLiquidator_bytecode, + // address _router, address _plb, address _ghoFlashMinter, address _ghoFMTaker, address _gho + args: [ + this.router, + this.bot, + this.ghoFlashMinter, + ghoFMTakerAddr, + tokenDataByNetwork[this.config.network].GHO, + ], + }); + this.logger.debug(`waiting for liquidator to deploy, tx hash: ${hash}`); + const { contractAddress: liquidatorAddr } = + await this.client.pub.waitForTransactionReceipt({ + hash, + timeout: 120_000, + }); + if (!liquidatorAddr) { + throw new Error(`liquidator was not deployed, tx hash: ${hash}`); + } + owner = await this.client.pub.readContract({ + abi: ghoLiquidatorAbi, + address: liquidatorAddr, + functionName: "owner", + }); + this.logger.debug( + `deployed Liquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, + ); + + const receipt = await this.client.simulateAndWrite({ + address: ghoFMTakerAddr, + abi: ghoFmTakerAbi, + functionName: "setAllowedFMReceiver", + args: [liquidatorAddr, true], + }); + if (receipt.status === "reverted") { + throw new Error( + `GhoFMTaker.setAllowedFMReceiver reverted, tx hash: ${receipt.transactionHash}`, + ); + } + this.logger.debug( + `set allowed flashloan receiver on FMTaker ${ghoFMTakerAddr} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, + ); + + address = liquidatorAddr; + } + this.logger.info(`partial liquidator contract addesss: ${address}`); + this.address = address; + } + + private get ghoFlashMinter(): Address { + if (this.config.network === "Mainnet") { + return "0xb639D208Bcf0589D54FaC24E655C79EC529762B8"; + } + throw new Error( + `gho flash minter is not available on ${this.config.network}`, + ); + } +} diff --git a/src/services/liquidate/PartialLiquidatorContract.ts b/src/services/liquidate/PartialLiquidatorContract.ts new file mode 100644 index 0000000..5017f26 --- /dev/null +++ b/src/services/liquidate/PartialLiquidatorContract.ts @@ -0,0 +1,273 @@ +import { iPartialLiquidatorAbi } from "@gearbox-protocol/liquidator-v2-contracts/abi"; +import { ADDRESS_0X0 } from "@gearbox-protocol/sdk-gov"; +import { iDegenDistributorV3Abi } from "@gearbox-protocol/types/abi"; +import type { Address } from "viem"; + +import type { Config } from "../../config/index.js"; +import type { CreditManagerData } from "../../data/index.js"; +import { DI } from "../../di.js"; +import type { ILogger } from "../../log/index.js"; +import type { AddressProviderService } from "../AddressProviderService.js"; +import type Client from "../Client.js"; +import type { MerkleDistributorInfo } from "./types.js"; + +export default abstract class PartialLiquidatorContract { + abstract logger: ILogger; + + @DI.Inject(DI.Config) + config!: Config; + + @DI.Inject(DI.AddressProvider) + addressProvider!: AddressProviderService; + + @DI.Inject(DI.Client) + client!: Client; + + #registeredCMs: Record = {}; + #address?: Address; + #router: Address; + #bot: Address; + #creditManagers: CreditManagerData[] = []; + + public readonly name: string; + + constructor(name: string, router: Address, bot: Address) { + this.name = name; + this.#router = router; + this.#bot = bot; + } + + /** + * Registers router, partial liquidation bot and credit manager addresses in liquidator contract if necessary + */ + public async configure(): Promise { + const [currentRouter, currentBot] = await Promise.all([ + this.client.pub.readContract({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "router", + }), + this.client.pub.readContract({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "partialLiquidationBot", + }), + ]); + + if (this.router.toLowerCase() !== currentRouter.toLowerCase()) { + this.logger.warn( + `need to update router from ${currentRouter} to ${this.router}`, + ); + const receipt = await this.client.simulateAndWrite({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "setRouter", + args: [this.router], + }); + if (receipt.status === "reverted") { + throw new Error( + `PartialLiquidator.setRouter(${this.router}) tx ${receipt.transactionHash} reverted`, + ); + } + this.logger.info( + `set router to ${this.router} in tx ${receipt.transactionHash}`, + ); + } + + if (this.bot.toLowerCase() !== currentBot.toLowerCase()) { + this.logger.warn(`need to update bot from ${currentBot} to ${this.bot}`); + const receipt = await this.client.simulateAndWrite({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "setPartialLiquidationBot", + args: [this.bot], + }); + if (receipt.status === "reverted") { + throw new Error( + `PartialLiquidator.setPartialLiquidationBot(${this.bot}) tx ${receipt.transactionHash} reverted`, + ); + } + this.logger.info( + `set bot to ${this.bot} in tx ${receipt.transactionHash}`, + ); + } + const cmToCa = await this.#getLiquidatorAccounts(this.#creditManagers); + + try { + await this.#claimDegenNFTs(cmToCa, this.#creditManagers); + } catch (e) { + this.logger.warn(`failed to obtain degen NFTs: ${e}`); + } + + for (const cm of this.#creditManagers) { + const { address, name } = cm; + const ca = cmToCa[address]; + if (ca === ADDRESS_0X0) { + await this.#registerCM(cm); + } else { + this.logger.debug( + `credit manager ${name} (${address}) already registered with account ${ca}`, + ); + this.#registeredCMs[address.toLowerCase() as Address] = true; + } + } + } + + public abstract deploy(): Promise; + + public addCreditManager(cm: CreditManagerData): void { + this.#creditManagers.push(cm); + } + + public get isSupported(): boolean { + return this.#creditManagers.length > 0; + } + + async #getLiquidatorAccounts( + cms: CreditManagerData[], + ): Promise> { + const results = await this.client.pub.multicall({ + allowFailure: false, + contracts: cms.map(cm => ({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "cmToCA", + args: [cm.address], + })), + }); + this.logger.debug(`loaded ${cms.length} liquidator credit accounts`); + return Object.fromEntries(cms.map((cm, i) => [cm.address, results[i]])); + } + + /** + * Claim NFT tokens as liquidator contract, so that the contract can open credit accounts in Degen NFT protected credit managers + * @param cmToCa + * @param cms + * @returns + */ + async #claimDegenNFTs( + cmToCa: Record, + cms: CreditManagerData[], + ): Promise { + const account = this.address; + + let nfts = 0; + for (const { address, name, degenNFT } of cms) { + if (cmToCa[address] === ADDRESS_0X0 && degenNFT !== ADDRESS_0X0) { + this.logger.debug( + `need degen NFT ${degenNFT} for credit manager ${name}`, + ); + nfts++; + } + } + if (nfts === 0) { + return; + } + + const distributor = await this.addressProvider.findService( + "DEGEN_DISTRIBUTOR", + 0, + 0, + ); + this.logger.debug(`degen distributor: ${distributor}`); + const [distributorNFT, merkelRoot, claimed] = + await this.client.pub.multicall({ + allowFailure: false, + contracts: [ + { + address: distributor, + abi: iDegenDistributorV3Abi, + functionName: "degenNFT", + }, + { + address: distributor, + abi: iDegenDistributorV3Abi, + functionName: "merkleRoot", + }, + { + address: distributor, + abi: iDegenDistributorV3Abi, + functionName: "claimed", + args: [account], + }, + ], + }); + const merkleRootURL = `https://dm.gearbox.finance/${this.config.network.toLowerCase()}_${merkelRoot}.json`; + this.logger.debug( + `merkle root: ${merkleRootURL}, degen distributor NFT: ${distributorNFT}, claimed: ${claimed}`, + ); + + const resp = await fetch(merkleRootURL); + const merkle = (await resp.json()) as MerkleDistributorInfo; + const claims = merkle.claims[account]; + if (!claims) { + throw new Error(`${account} is not eligible for degen NFT claim`); + } + this.logger.debug(claims, `claims`); + if (BigInt(claims.amount) <= claimed) { + throw new Error(`already claimed`); + } + + const receipt = await this.client.simulateAndWrite({ + address: distributor, + abi: iDegenDistributorV3Abi, + functionName: "claim", + args: [ + BigInt(claims.index), // uint256 index, + account, // address account, + BigInt(claims.amount), // uint256 totalAmount, + claims.proof, // bytes32[] calldata merkleProof + ], + }); + if (receipt.status === "reverted") { + throw new Error(`degenDistributor.claim reverted`); + } + this.logger.debug(`${account} claimed ${BigInt(claims.amount)} degenNFTs`); + } + + async #registerCM(cm: CreditManagerData): Promise { + const { address, name } = cm; + try { + this.logger.debug(`need to register credit manager ${name} (${address})`); + const receipt = await this.client.simulateAndWrite({ + abi: iPartialLiquidatorAbi, + address: this.address, + functionName: "registerCM", + args: [address], + }); + if (receipt.status === "reverted") { + throw new Error( + `Liquidator.registerCM(${address}) reverted: ${receipt.transactionHash}`, + ); + } + this.logger.info( + `registered credit manager ${name} (${address}) in tx ${receipt.transactionHash}`, + ); + this.#registeredCMs[address.toLowerCase() as Address] = true; + } catch (e) { + this.logger.error( + `failed to register credit manager ${name} (${address}): ${e}`, + ); + this.#registeredCMs[address.toLowerCase() as Address] = false; + } + } + + protected set address(value: Address) { + this.#address = value; + } + + public get address(): Address { + if (!this.#address) { + throw new Error("liquidator contract address not set"); + } + return this.#address; + } + + protected get router(): Address { + return this.#router; + } + + protected get bot(): Address { + return this.#bot; + } +} diff --git a/src/services/liquidate/SingularPartialLiquidator.ts b/src/services/liquidate/SingularPartialLiquidator.ts index bddb8a9..8f661b2 100644 --- a/src/services/liquidate/SingularPartialLiquidator.ts +++ b/src/services/liquidate/SingularPartialLiquidator.ts @@ -1,22 +1,15 @@ import { - aaveFlTakerAbi, - iLiquidatorAbi, + iPartialLiquidatorAbi, iPriceHelperAbi, - liquidatorAbi, priceHelperAbi, } from "@gearbox-protocol/liquidator-v2-contracts/abi"; -import { - AaveFLTaker_bytecode, - Liquidator_bytecode, - PriceHelper_bytecode, -} from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; +import { PriceHelper_bytecode } from "@gearbox-protocol/liquidator-v2-contracts/bytecode"; import type { ExcludeArrayProps } from "@gearbox-protocol/sdk-gov"; import { - ADDRESS_0X0, - contractsByNetwork, formatBN, getDecimals, PERCENTAGE_FACTOR, + tokenDataByNetwork, tokenSymbolByAddress, WAD, } from "@gearbox-protocol/sdk-gov"; @@ -24,7 +17,6 @@ import { iaclAbi, iCreditConfiguratorV3Abi, iCreditManagerV3Abi, - iDegenDistributorV3Abi, } from "@gearbox-protocol/types/abi"; import type { Address, SimulateContractReturnType } from "viem"; import { parseEther } from "viem"; @@ -35,10 +27,12 @@ import { exceptionsAbis, } from "../../data/index.js"; import type { ILogger } from "../../log/index.js"; +import AAVELiquidatorContract from "./AAVELiquidatorContract.js"; +import GHOLiquidatorContract from "./GHOLiquidatorContract.js"; +import type PartialLiquidatorContract from "./PartialLiquidatorContract.js"; import SingularLiquidator from "./SingularLiquidator.js"; import type { MakeLiquidatableResult, - MerkleDistributorInfo, PartialLiquidationPreview, } from "./types.js"; import type { TokenPriceInfo } from "./viem-types.js"; @@ -54,10 +48,12 @@ export default class SingularPartialLiquidator extends SingularLiquidator = {}; + /** + * mapping of credit manager address to deployed partial liquidator + */ + #liquidatorForCM: Record = {}; public async launch(): Promise { await super.launch(); @@ -67,13 +63,34 @@ export default class SingularPartialLiquidator extends SingularLiquidator { + const liquidatorAddr = this.liquidatorForCA(account); + if (!liquidatorAddr) { + throw new Error( + `no partial liquidator contract found for account ${account.addr} in ${account.cmName}`, + ); + } return this.client.pub.simulateContract({ account: this.client.account, - address: this.partialLiquidator, - abi: [...iLiquidatorAbi, ...exceptionsAbis], + address: liquidatorAddr, + abi: [...iPartialLiquidatorAbi, ...exceptionsAbis], functionName: "partialLiquidateAndConvert", args: [ account.creditManager, @@ -349,84 +378,6 @@ export default class SingularPartialLiquidator extends SingularLiquidator { - let partialLiquidatorAddress = this.config.partialLiquidatorAddress; - if (!partialLiquidatorAddress) { - this.logger.debug("deploying partial liquidator"); - - let hash = await this.client.wallet.deployContract({ - abi: aaveFlTakerAbi, - bytecode: AaveFLTaker_bytecode, - args: [aavePool], - }); - this.logger.debug(`waiting for AaveFLTaker to deploy, tx hash: ${hash}`); - const { contractAddress: aaveFlTakerAddr } = - await this.client.pub.waitForTransactionReceipt({ - hash, - timeout: 120_000, - }); - if (!aaveFlTakerAddr) { - throw new Error(`AaveFLTaker was not deployed, tx hash: ${hash}`); - } - let owner = await this.client.pub.readContract({ - abi: aaveFlTakerAbi, - functionName: "owner", - address: aaveFlTakerAddr, - }); - this.logger.debug( - `deployed AaveFLTaker at ${aaveFlTakerAddr} owned by ${owner} in tx ${hash}`, - ); - - hash = await this.client.wallet.deployContract({ - abi: liquidatorAbi, - bytecode: Liquidator_bytecode, - args: [router, bot, aavePool, aaveFlTakerAddr], - }); - this.logger.debug(`waiting for liquidator to deploy, tx hash: ${hash}`); - const { contractAddress: liquidatorAddr } = - await this.client.pub.waitForTransactionReceipt({ - hash, - timeout: 120_000, - }); - if (!liquidatorAddr) { - throw new Error(`Liquidator was not deployed, tx hash: ${hash}`); - } - owner = await this.client.pub.readContract({ - abi: liquidatorAbi, - address: liquidatorAddr, - functionName: "owner", - }); - this.logger.debug( - `deployed Liquidator at ${liquidatorAddr} owned by ${owner} in tx ${hash}`, - ); - - const receipt = await this.client.simulateAndWrite({ - address: aaveFlTakerAddr, - abi: aaveFlTakerAbi, - functionName: "setAllowedFLReceiver", - args: [liquidatorAddr, true], - }); - if (receipt.status === "reverted") { - throw new Error( - `AaveFLTaker.setAllowedFLReceiver reverted, tx hash: ${receipt.transactionHash}`, - ); - } - this.logger.debug( - `set allowed flashloan receiver on FLTaker ${aaveFlTakerAddr} to ${liquidatorAddr} in tx ${receipt.transactionHash}`, - ); - - partialLiquidatorAddress = liquidatorAddr; - } - this.logger.info( - `partial liquidator contract addesss: ${partialLiquidatorAddress}`, - ); - this.#partialLiquidator = partialLiquidatorAddress; - } - async #deployPriceHelper(): Promise { if (!this.config.optimistic) { return undefined; @@ -453,210 +404,6 @@ export default class SingularPartialLiquidator extends SingularLiquidator { - const [currentRouter, currentBot, cms] = await Promise.all([ - this.client.pub.readContract({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "router", - }), - this.client.pub.readContract({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "partialLiquidationBot", - }), - this.getCreditManagersV3List(), - ]); - - if (router.toLowerCase() !== currentRouter.toLowerCase()) { - this.logger.warn( - `need to update router from ${currentRouter} to ${router}`, - ); - const receipt = await this.client.simulateAndWrite({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "setRouter", - args: [router], - }); - if (receipt.status === "reverted") { - throw new Error( - `PartialLiquidator.setRouter(${router}) tx ${receipt.transactionHash} reverted`, - ); - } - this.logger.info( - `set router to ${router} in tx ${receipt.transactionHash}`, - ); - } - - if (bot.toLowerCase() !== currentBot.toLowerCase()) { - this.logger.warn(`need to update bot from ${currentBot} to ${bot}`); - const receipt = await this.client.simulateAndWrite({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "setPartialLiquidationBot", - args: [bot], - }); - if (receipt.status === "reverted") { - throw new Error( - `PartialLiquidator.setPartialLiquidationBot(${bot}) tx ${receipt.transactionHash} reverted`, - ); - } - this.logger.info(`set bot to ${bot} in tx ${receipt.transactionHash}`); - } - const cmToCa = await this.#getLiquidatorAccounts(cms); - - try { - await this.#claimDegenNFTs(cmToCa, cms); - } catch (e) { - this.logger.warn(`failed to obtain degen NFTs: ${e}`); - } - - for (const cm of cms) { - const { address, name } = cm; - const ca = cmToCa[address]; - if (ca === ADDRESS_0X0) { - await this.#registerCM(cm); - } else { - this.logger.debug( - `credit manager ${name} (${address}) already registered with account ${ca}`, - ); - this.#registeredCMs[address.toLowerCase() as Address] = true; - } - } - } - - async #getLiquidatorAccounts( - cms: CreditManagerData[], - ): Promise> { - const results = await this.client.pub.multicall({ - allowFailure: false, - contracts: cms.map(cm => ({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "cmToCA", - args: [cm.address], - })), - }); - this.logger.debug(`loaded ${cms.length} liquidator credit accounts`); - return Object.fromEntries(cms.map((cm, i) => [cm.address, results[i]])); - } - - /** - * Claim NFT tokens as liquidator contract, so that the contract can open credit accounts in Degen NFT protected credit managers - * @param cmToCa - * @param cms - * @returns - */ - async #claimDegenNFTs( - cmToCa: Record, - cms: CreditManagerData[], - ): Promise { - const account = this.partialLiquidator; - - let nfts = 0; - for (const { address, name, degenNFT } of cms) { - if (cmToCa[address] === ADDRESS_0X0 && degenNFT !== ADDRESS_0X0) { - this.logger.debug( - `need degen NFT ${degenNFT} for credit manager ${name}`, - ); - nfts++; - } - } - if (nfts === 0) { - return; - } - - const distributor = await this.addressProvider.findService( - "DEGEN_DISTRIBUTOR", - 0, - 0, - ); - this.logger.debug(`degen distributor: ${distributor}`); - const [distributorNFT, merkelRoot, claimed] = - await this.client.pub.multicall({ - allowFailure: false, - contracts: [ - { - address: distributor, - abi: iDegenDistributorV3Abi, - functionName: "degenNFT", - }, - { - address: distributor, - abi: iDegenDistributorV3Abi, - functionName: "merkleRoot", - }, - { - address: distributor, - abi: iDegenDistributorV3Abi, - functionName: "claimed", - args: [account], - }, - ], - }); - const merkleRootURL = `https://dm.gearbox.finance/${this.config.network.toLowerCase()}_${merkelRoot}.json`; - this.logger.debug( - `merkle root: ${merkleRootURL}, degen distributor NFT: ${distributorNFT}, claimed: ${claimed}`, - ); - - const resp = await fetch(merkleRootURL); - const merkle = (await resp.json()) as MerkleDistributorInfo; - const claims = merkle.claims[account]; - if (!claims) { - throw new Error(`${account} is not eligible for degen NFT claim`); - } - this.logger.debug(claims, `claims`); - if (BigInt(claims.amount) <= claimed) { - throw new Error(`already claimed`); - } - - const receipt = await this.client.simulateAndWrite({ - address: distributor, - abi: iDegenDistributorV3Abi, - functionName: "claim", - args: [ - BigInt(claims.index), // uint256 index, - account, // address account, - BigInt(claims.amount), // uint256 totalAmount, - claims.proof, // bytes32[] calldata merkleProof - ], - }); - if (receipt.status === "reverted") { - throw new Error(`degenDistributor.claim reverted`); - } - this.logger.debug(`${account} claimed ${BigInt(claims.amount)} degenNFTs`); - } - - async #registerCM(cm: CreditManagerData): Promise { - const { address, name } = cm; - try { - this.logger.debug(`need to register credit manager ${name} (${address})`); - const receipt = await this.client.simulateAndWrite({ - abi: iLiquidatorAbi, - address: this.partialLiquidator, - functionName: "registerCM", - args: [address], - }); - if (receipt.status === "reverted") { - throw new Error( - `Liquidator.registerCM(${address}) reverted: ${receipt.transactionHash}`, - ); - } - this.logger.info( - `registered credit manager ${name} (${address}) in tx ${receipt.transactionHash}`, - ); - this.#registeredCMs[address.toLowerCase() as Address] = true; - } catch (e) { - this.logger.error( - `failed to register credit manager ${name} (${address}): ${e}`, - ); - this.#registeredCMs[address.toLowerCase() as Address] = false; - } - } - #caLogger(ca: CreditAccountData): ILogger { return this.logger.child({ account: ca.addr, @@ -666,13 +413,6 @@ export default class SingularPartialLiquidator extends SingularLiquidator { config!: Config; produce(): ILiquidatorService { - const { - deployBatchLiquidatorContracts, - deployPartialLiquidatorContracts, - partialLiquidatorAddress, - batchLiquidatorAddress, - } = this.config; - if (deployPartialLiquidatorContracts || partialLiquidatorAddress) { + if (this.config.isPartial) { return new SingularPartialLiquidator(); } - if (deployBatchLiquidatorContracts || batchLiquidatorAddress) { + if (this.config.isBatch) { return new BatchLiquidator(); } return new SingularFullLiquidator(); diff --git a/src/services/liquidate/viem-types.ts b/src/services/liquidate/viem-types.ts index 15cc490..94611e6 100644 --- a/src/services/liquidate/viem-types.ts +++ b/src/services/liquidate/viem-types.ts @@ -1,6 +1,6 @@ import type { iBatchLiquidatorAbi, - iLiquidatorAbi, + iPartialLiquidatorAbi, iPriceHelperAbi, } from "@gearbox-protocol/liquidator-v2-contracts/abi"; import type { AbiParameterToPrimitiveType, ExtractAbiFunction } from "abitype"; @@ -13,8 +13,8 @@ export type IPriceHelperContract = GetContractReturnType< PublicClient >; -export type ILiquidatorContract = GetContractReturnType< - typeof iLiquidatorAbi, +export type IPartialLiquidatorContract = GetContractReturnType< + typeof iPartialLiquidatorAbi, PublicClient >; diff --git a/src/services/scanner/Scanner.ts b/src/services/scanner/Scanner.ts index 0080895..429aa4c 100644 --- a/src/services/scanner/Scanner.ts +++ b/src/services/scanner/Scanner.ts @@ -170,18 +170,10 @@ export class Scanner { priceUpdates: PriceOnDemand[], blockNumber?: bigint, ): Promise<[accounts: CreditAccountData[], failedTokens: Address[]]> { - const { - optimistic, - debugAccounts, - debugManagers, - deployPartialLiquidatorContracts, - partialLiquidatorAddress, - } = this.config; + const { optimistic, debugAccounts, debugManagers } = this.config; // during partial + optimistic liquidation, liquidation condition is not created externally. // it's created by liquidator itself before the liquidation. - const liquidatableOnly = - !optimistic || - (!deployPartialLiquidatorContracts && !partialLiquidatorAddress); + const liquidatableOnly = !optimistic || !this.config.isPartial; const selection: AccountSelection = { liquidatableOnly, diff --git a/src/utils/index.ts b/src/utils/index.ts index c50cb7f..d972d60 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./detect-network.js"; export * from "./etherscan.js"; export * from "./formatters.js"; export * from "./types.js"; +export * from "./typescript.js"; diff --git a/src/utils/typescript.ts b/src/utils/typescript.ts new file mode 100644 index 0000000..7eacfdb --- /dev/null +++ b/src/utils/typescript.ts @@ -0,0 +1,11 @@ +/** + * Typescript helper for converting json interfaces to classes + * @returns + */ +export function createClassFromType() { + return class { + constructor(args: T) { + Object.assign(this, args); + } + } as new (args: T) => T; +} diff --git a/yarn.lock b/yarn.lock index 4e4970f..e699725 100644 --- a/yarn.lock +++ b/yarn.lock @@ -640,23 +640,23 @@ "@openzeppelin/contracts-upgradeable" "^4.7.3" "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" -"@commitlint/cli@^19.4.0": - version "19.4.0" - resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.4.0.tgz#9f93d3ed07e531fcfa371015c8c87e0aa26d974f" - integrity sha512-sJX4J9UioVwZHq7JWM9tjT5bgWYaIN3rC4FP7YwfEwBYiIO+wMyRttRvQLNkow0vCdM0D67r9NEWU0Ui03I4Eg== +"@commitlint/cli@^19.4.1": + version "19.4.1" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.4.1.tgz#51dbd88750620c9e5fb6f5bc773872728a29674a" + integrity sha512-EerFVII3ZcnhXsDT9VePyIdCJoh3jEzygN1L37MjQXgPfGS6fJTWL/KHClVMod1d8w94lFC3l4Vh/y5ysVAz2A== dependencies: "@commitlint/format" "^19.3.0" - "@commitlint/lint" "^19.2.2" + "@commitlint/lint" "^19.4.1" "@commitlint/load" "^19.4.0" "@commitlint/read" "^19.4.0" "@commitlint/types" "^19.0.3" execa "^8.0.1" yargs "^17.0.0" -"@commitlint/config-conventional@^19.2.2": - version "19.2.2" - resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-19.2.2.tgz#1f4e6975d428985deacf2b3ff6547e02c9302054" - integrity sha512-mLXjsxUVLYEGgzbxbxicGPggDuyWNkf25Ht23owXIH+zV2pv1eJuzLK3t1gDY5Gp6pxdE60jZnWUY5cvgL3ufw== +"@commitlint/config-conventional@^19.4.1": + version "19.4.1" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-19.4.1.tgz#c6f05d478c7576d5affff82d67d9ca37e96c94e6" + integrity sha512-D5S5T7ilI5roybWGc8X35OBlRXLAwuTseH1ro0XgqkOWrhZU8yOwBOslrNmSDlTXhXLq8cnfhQyC42qaUCzlXA== dependencies: "@commitlint/types" "^19.0.3" conventional-changelog-conventionalcommits "^7.0.2" @@ -702,14 +702,14 @@ "@commitlint/types" "^19.0.3" semver "^7.6.0" -"@commitlint/lint@^19.2.2": - version "19.2.2" - resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-19.2.2.tgz#57f69e24bd832a7dcce8ebf82d11e3bf03ccc2a9" - integrity sha512-xrzMmz4JqwGyKQKTpFzlN0dx0TAiT7Ran1fqEBgEmEj+PU98crOFtysJgY+QdeSagx6EDRigQIXJVnfrI0ratA== +"@commitlint/lint@^19.4.1": + version "19.4.1" + resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-19.4.1.tgz#0760d34fcdaee0bf05befe666ca14c0fc1ecb57e" + integrity sha512-Ws4YVAZ0jACTv6VThumITC1I5AG0UyXMGua3qcf55JmXIXm/ejfaVKykrqx7RyZOACKVAs8uDRIsEsi87JZ3+Q== dependencies: "@commitlint/is-ignored" "^19.2.2" "@commitlint/parse" "^19.0.3" - "@commitlint/rules" "^19.0.3" + "@commitlint/rules" "^19.4.1" "@commitlint/types" "^19.0.3" "@commitlint/load@^19.4.0": @@ -765,10 +765,10 @@ lodash.mergewith "^4.6.2" resolve-from "^5.0.0" -"@commitlint/rules@^19.0.3": - version "19.0.3" - resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-19.0.3.tgz#de647a9055847cae4f3ae32b4798096b604584f3" - integrity sha512-TspKb9VB6svklxNCKKwxhELn7qhtY1rFF8ls58DcFd0F97XoG07xugPjjbVnLqmMkRjZDbDIwBKt9bddOfLaPw== +"@commitlint/rules@^19.4.1": + version "19.4.1" + resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-19.4.1.tgz#df15baad1092e2be1b39aa1aa7cc05e12f59f677" + integrity sha512-AgctfzAONoVxmxOXRyxXIq7xEPrd7lK/60h2egp9bgGUMZK9v0+YqLOA+TH+KqCa63ZoCr8owP2YxoSSu7IgnQ== dependencies: "@commitlint/ensure" "^19.0.3" "@commitlint/message" "^19.0.0" @@ -1576,20 +1576,20 @@ eslint-plugin-simple-import-sort "^10.0.0" eslint-plugin-unused-imports "^3.0.0" -"@gearbox-protocol/liquidator-v2-contracts@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@gearbox-protocol/liquidator-v2-contracts/-/liquidator-v2-contracts-2.1.0.tgz#64a7458eb0df1caf5688d243d5f678f43db09760" - integrity sha512-Xn8LvC2ddn1p1QsTNt1n+fpFfCvZuX84CK1+lKorgimNmnMMgLCWZXuX+dZRNjxA74lvhmco9Xcr1aab4wg/Aw== +"@gearbox-protocol/liquidator-v2-contracts@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@gearbox-protocol/liquidator-v2-contracts/-/liquidator-v2-contracts-2.2.1.tgz#af5c375b7e11d1e3b466298bc8b518d124abdd81" + integrity sha512-ybyA+pwgAgaEgil6m5nsP01/f+6/sc3nnQo3byCI03z7/rTeQwiKdK3Os+WIx837Uy8nqpVUxuQhIhBq33Hlbw== "@gearbox-protocol/prettier-config@2.0.0-next.0": version "2.0.0-next.0" resolved "https://registry.yarnpkg.com/@gearbox-protocol/prettier-config/-/prettier-config-2.0.0-next.0.tgz#8183cfa8c6ee538543089961ecb4d03fe77045de" integrity sha512-hDokre6TjEkpNdf+tTk/Gh2dTJpkJFgMPTpE7KS4KFddUqGLqDKMaE4/ZzBA8kvYNm5gSXytCwWrxPXO8kFKYA== -"@gearbox-protocol/sdk-gov@^2.18.5": - version "2.18.5" - resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk-gov/-/sdk-gov-2.18.5.tgz#325d1fcc3d941a70bd740efdcf9acfd6e2b4df93" - integrity sha512-zsXlvPSh/rAyGw022o+iz73uBHzWENI/dMOc7N2theEDmMiU1vx7gXZofpZa9Frofk24U1EyFtRZmpd22g+O+Q== +"@gearbox-protocol/sdk-gov@^2.20.0": + version "2.20.0" + resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk-gov/-/sdk-gov-2.20.0.tgz#b0979c13e4b35bb759774e4fc379e23b3603f703" + integrity sha512-HZdf3yrzBzbs2yxridBLTvlB/mDGZ5FSlGqs458SqxEyptD7ONlAifCAS2DAhhXF8fe9k5pFSJhVOEmAkBtF3g== dependencies: ethers "6.12.1" humanize-duration-ts "^2.1.1" @@ -2413,10 +2413,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== -"@types/node@^22.5.0": - version "22.5.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958" - integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg== +"@types/node@^22.5.2": + version "22.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.2.tgz#e42344429702e69e28c839a7e16a8262a8086793" + integrity sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg== dependencies: undici-types "~6.19.2" @@ -2909,10 +2909,10 @@ axios@^1.6.2: form-data "^4.0.0" proxy-from-env "^1.1.0" -axios@^1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1" - integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw== +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -4731,10 +4731,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^15.2.9: - version "15.2.9" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.9.tgz#bf70d40b6b192df6ad756fb89822211615e0f4da" - integrity sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ== +lint-staged@^15.2.10: + version "15.2.10" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2" + integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg== dependencies: chalk "~5.3.0" commander "~12.1.0" @@ -4742,7 +4742,7 @@ lint-staged@^15.2.9: execa "~8.0.1" lilconfig "~3.1.2" listr2 "~8.2.4" - micromatch "~4.0.7" + micromatch "~4.0.8" pidtree "~0.6.0" string-argv "~0.3.2" yaml "~2.5.0" @@ -4877,7 +4877,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4, micromatch@~4.0.7: +micromatch@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== @@ -4885,6 +4885,14 @@ micromatch@^4.0.4, micromatch@~4.0.7: braces "^3.0.3" picomatch "^2.3.1" +micromatch@~4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -5969,10 +5977,10 @@ uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -viem@^2.20.1: - version "2.20.1" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.20.1.tgz#91742e19c24e6294cf5c4015f1cba46afd0b95d7" - integrity sha512-a/BSe25TSfkc423GTSKYl1O0ON2J5huoQeOLkylHT1WS8wh3JFqb8nfAq7vg+aZ+W06BCTn36bbi47yp4D92Cg== +viem@^2.21.1: + version "2.21.1" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.1.tgz#7d07e4302297d1b44b4c97b8fece3b6ebb73b1ef" + integrity sha512-nlIc2LLS6aqkngULS9UJ2Sg3nHKAgF9bbpDUwjUoAUBijd69mrCWPBXQ8jmbzcx12uZUfd9Nc//CHgSVZiMwyg== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.4.0"