├── src ├── database.rs ├── consts.rs ├── actors │ ├── messages │ │ ├── mod.rs │ │ ├── executor.rs │ │ ├── follower.rs │ │ └── fanatic.rs │ ├── mod.rs │ ├── database.rs │ ├── executor.rs │ ├── follower.rs │ └── fanatic.rs ├── main.rs ├── args.rs ├── configs.rs ├── contracts.rs ├── run.rs └── utils.rs ├── .gitignore ├── contracts ├── .gitignore ├── foundry.toml └── src │ └── Liquidator.sol ├── docker-compose.yml ├── .gitmodules ├── Cargo.toml ├── migrations ├── 20240101010102_insert_val.sql └── 20240101010101_create_tables.sql ├── abis ├── aave_v3 │ ├── Pool.json │ ├── PoolAddressesProvider.json │ ├── AaveProtocolDataProvider.json │ └── UIPoolDataProviderV3.json ├── chainlink │ ├── CLSynchronicityPriceAdapterPegToBase.json │ ├── PriceCapAdapterStable.json │ ├── CLRatePriceCapAdapter.json │ └── EACAggregatorProxy.json └── uniswap_v3 │ ├── UniswapV3Factory.json │ ├── QuoterV2.json │ └── UniswapV3Pool.json └── README.md /src/database.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | //! https://aave.com/docs/resources/parameters 2 | 3 | pub const RAY: f64 = 1e27; 4 | -------------------------------------------------------------------------------- /src/actors/messages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | pub mod executor; 3 | pub mod fanatic; 4 | pub mod follower; 5 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | broadcast/ 2 | cache/ 3 | logs/ 4 | out/ 5 | zkout/ 6 | 7 | **/broadcast/ 8 | **/cache/ 9 | **/logs 10 | **/out/ 11 | **/zkout/ 12 | lotus-router/lib 13 | -------------------------------------------------------------------------------- /src/actors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod messages; 2 | 3 | pub mod database; 4 | pub mod executor; 5 | pub mod fanatic; 6 | pub mod follower; 7 | 8 | pub use database::Database; 9 | pub use executor::Executor; 10 | pub use fanatic::Fanatic; 11 | pub use follower::Follower; 12 | -------------------------------------------------------------------------------- /src/actors/messages/executor.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use alloy::{primitives::Address, providers::Provider}; 3 | 4 | use crate::actors::Fanatic; 5 | 6 | #[derive(Message, Debug, Clone)] 7 | #[rtype(result = "eyre::Result<()>")] 8 | pub struct LiquidationRequest { 9 | pub user_address: Address, 10 | pub network: String, 11 | pub protocol: String, 12 | } 13 | -------------------------------------------------------------------------------- /src/actors/database.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::configs::DatabaseConfig; 4 | use actix::prelude::*; 5 | use sqlx::{PgPool, Row}; 6 | 7 | pub struct Database { 8 | pub pool: Arc, 9 | } 10 | 11 | impl Actor for Database { 12 | type Context = Context; 13 | } 14 | 15 | impl Database { 16 | pub async fn new(config: DatabaseConfig) -> Database { 17 | Database { pool: config.pool } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | local_postgres_data: 3 | 4 | services: 5 | postgres: 6 | image: postgres:16 7 | container_name: postgres 8 | restart: always 9 | environment: 10 | POSTGRES_USER: user 11 | POSTGRES_PASSWORD: password 12 | POSTGRES_DB: liquidator 13 | POSTGRES_HOST_AUTH_METHOD: trust 14 | ports: 15 | - "5432:5432" 16 | volumes: 17 | - local_postgres_data:/var/lib/postgresql/data 18 | -------------------------------------------------------------------------------- /src/actors/messages/follower.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use alloy::providers::Provider; 3 | 4 | use crate::actors::Fanatic; 5 | 6 | #[derive(Message, Debug, Clone)] 7 | #[rtype(result = "()")] 8 | pub struct StartListeningForOraclePrices; 9 | 10 | #[derive(Message, Debug, Clone)] 11 | #[rtype(result = "()")] 12 | pub struct StartListeningForEvents; 13 | 14 | #[derive(Message, Debug, Clone)] 15 | #[rtype(result = "()")] 16 | pub struct SendFanaticAddr(pub Addr>); 17 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | remappings = [ 7 | # makes dependency import cleaner 8 | "@=./src", 9 | 10 | # dependencies - general 11 | "@forge-std=lib/forge-std/src", 12 | "@openzeppelin=lib/openzeppelin/contracts", 13 | 14 | # dependencies - protocols 15 | "@aave-v3=lib/aave-v3/src/contracts", 16 | "@uniswap-v3-periphery=lib/uniswap-v3-periphery/contracts", 17 | ] 18 | 19 | [fmt] 20 | line_length = 120 21 | tab_width = 4 22 | bracket_spacing = false 23 | 24 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 25 | [rpc_endpoints] 26 | ethereum = "https://eth.merkle.io" 27 | 28 | [etherscan] 29 | mainnet = { key = "${ETHERSCAN_ETHEREUM_API_KEY}" } 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/forge-std"] 2 | path = contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std.git 4 | 5 | [submodule "contracts/lib/solady"] 6 | path = contracts/lib/solady 7 | url = https://github.com/Vectorized/solady 8 | 9 | [submodule "contracts/lib/openzeppelin"] 10 | path = contracts/lib/openzeppelin 11 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 12 | 13 | [submodule "contracts/lib/aave-v3"] 14 | path = contracts/lib/aave-v3 15 | url = https://github.com/aave-dao/aave-v3-origin 16 | 17 | [submodule "contracts/lib/balancer-v3"] 18 | path = contracts/lib/balancer-v3 19 | url = https://github.com/balancer/balancer-v3-monorepo 20 | 21 | [submodule "contracts/lib/uniswap-v3-periphery"] 22 | path = contracts/lib/uniswap-v3-periphery 23 | url = https://github.com/uniswap/v3-periphery 24 | 25 | [submodule "contracts/lib/uniswap-v3-core"] 26 | path = contracts/lib/uniswap-v3-core 27 | url = https://github.com/uniswap/v3-core 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "liquidator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | actix = "0.13.5" 8 | actix-rt = "2.10.0" 9 | actix-web = "4.9.0" 10 | alloy = { version = "0.11", features = [ 11 | "full", 12 | "provider-ws", 13 | "providers", 14 | "rpc", 15 | "provider-anvil-node", 16 | "sol-types", 17 | ] } 18 | alloy-contract = "0.11.0" 19 | alloy-sol-types = "0.8.20" 20 | clap = { version = "4.5.27", features = ["derive", "env", "color", "std"] } 21 | eyre = "0.6.12" 22 | futures = "0.3.31" 23 | futures-util = "0.3.31" 24 | secrecy = "0.10.3" 25 | serde = { version = "1.0.217", features = ["derive"] } 26 | serde_json = "1.0.138" 27 | sqlx = { version = "0.8.3", features = [ 28 | "postgres", 29 | "runtime-tokio-native-tls", 30 | "time", 31 | "bigdecimal", 32 | ] } 33 | tokio = { version = "1.43.0", features = ["full"] } 34 | tracing = "0.1.41" 35 | tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] } 36 | -------------------------------------------------------------------------------- /src/actors/messages/fanatic.rs: -------------------------------------------------------------------------------- 1 | use crate::{actors::Executor, contracts}; 2 | use actix::prelude::*; 3 | use alloy::primitives::Address; 4 | use alloy::providers::Provider; 5 | 6 | #[derive(Message, Debug, Clone)] 7 | #[rtype(result = "()")] 8 | pub struct UpdateReservePrice { 9 | pub reserve: Address, 10 | pub new_price: f64, 11 | } 12 | 13 | #[derive(Message, Debug, Clone)] 14 | #[rtype(result = "()")] 15 | pub struct UpdateReserveUser { 16 | pub reserve: Address, 17 | pub user_addr: Address, 18 | } 19 | 20 | #[derive(Message)] 21 | #[rtype(result = "eyre::Result<()>")] 22 | pub struct DoSmthWithLiquidationCall(pub contracts::aave_v3::PoolContract::LiquidationCall); 23 | 24 | #[derive(Message)] 25 | #[rtype(result = "eyre::Result<()>")] 26 | pub struct SuccessfulLiquidation { 27 | pub user_addr: Address, 28 | } 29 | 30 | #[derive(Message)] 31 | #[rtype(result = "eyre::Result<()>")] 32 | pub struct FailedLiquidation { 33 | pub user_addr: Address, 34 | } 35 | 36 | #[derive(Message, Debug, Clone)] 37 | #[rtype(result = "()")] 38 | pub struct SendExecutorAddr(pub Addr>); 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod actors; 2 | mod args; 3 | mod configs; 4 | mod consts; 5 | mod contracts; 6 | mod database; 7 | mod run; 8 | mod utils; 9 | 10 | use clap::Parser; 11 | use secrecy::ExposeSecret; 12 | use tracing::{debug, info}; 13 | 14 | use crate::{args::Args, configs::Config, run::run}; 15 | 16 | pub async fn run_migrations(config: &Config) { 17 | sqlx::migrate!("./migrations") 18 | .run(&sqlx::PgPool::connect(&config.database_url).await.unwrap()) 19 | .await 20 | .unwrap(); 21 | } 22 | 23 | #[actix_rt::main] 24 | async fn main() { 25 | tracing_subscriber::fmt().init(); 26 | 27 | let args = Args::parse(); 28 | info!(?args); 29 | 30 | let config = Config { 31 | ws_url: args.ws_url.expose_secret().into(), 32 | database_url: args.database_url.expose_secret().into(), 33 | account_pubkey: args.account_pubkey, 34 | account_privkey: args.account_privkey.expose_secret().into(), 35 | bot_addr: args.bot_addr, 36 | target: format!("{}-{}", args.network, args.protocol), 37 | }; 38 | debug!(?config); 39 | 40 | run_migrations(&config).await; 41 | run(config).await 42 | } 43 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Address; 2 | use clap::Parser; 3 | use secrecy::SecretString; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(author, version, about, long_about = None)] 7 | pub struct Args { 8 | #[arg(long, env = "WS_URL", default_value = "ws://localhost:8545")] 9 | pub ws_url: SecretString, 10 | 11 | #[arg( 12 | long, 13 | env = "DATABASE_URL", 14 | default_value = "postgres://user:password@localhost:5432/liquidator" 15 | )] 16 | pub database_url: SecretString, 17 | 18 | #[arg( 19 | long, 20 | env = "NETWORK", 21 | help = "The network ID to connect to (e.g., ethereum, polygon, zksync)" 22 | )] 23 | pub network: String, 24 | 25 | #[arg( 26 | long, 27 | env = "PROTOCOL", 28 | help = "The protocol ID (e.g., aave_v3, spark (spark's an aave fork))" 29 | )] 30 | pub protocol: String, 31 | 32 | #[arg(long, env = "ACCOUNT_PUBKEY")] 33 | pub account_pubkey: Address, 34 | 35 | #[arg(long, env = "ACCOUNT_PRIVKEY")] 36 | pub account_privkey: SecretString, 37 | 38 | #[arg(long, env = "BOT_ADDR")] 39 | pub bot_addr: Address, 40 | } 41 | -------------------------------------------------------------------------------- /src/configs.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use actix::Addr; 4 | use alloy::{primitives::Address, providers::Provider}; 5 | use sqlx::PgPool; 6 | 7 | use crate::actors::{Database, Fanatic, Follower}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Config { 11 | pub ws_url: String, 12 | pub database_url: String, 13 | pub target: String, 14 | pub account_pubkey: Address, 15 | pub account_privkey: String, 16 | pub bot_addr: Address, 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct FollowerConfig { 21 | pub provider: P, 22 | pub db_addr: Addr, 23 | pub target: String, 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct ExecutorConfig { 28 | pub provider: P, 29 | pub db_addr: Addr, 30 | pub fanatic_addr: Addr>, 31 | pub bot_addr: Address, 32 | pub target: String, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct FanaticConfig { 37 | pub provider: P, 38 | pub db_addr: Addr, 39 | pub follower_addr: Addr>, 40 | pub target: String, 41 | } 42 | 43 | #[derive(Debug, Clone)] 44 | pub struct DatabaseConfig { 45 | pub pool: Arc, 46 | } 47 | -------------------------------------------------------------------------------- /migrations/20240101010102_insert_val.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO 2 | networks (id, chain_id) 3 | VALUES 4 | ('ethereum', 1); 5 | 6 | -- The protocol (i.e aave_v3) 7 | INSERT INTO 8 | protocols (id, name, kind) 9 | VALUES 10 | ('aave_v3', 'Aave V3', 'lending'); 11 | 12 | -------------- ethereum -------------- 13 | WITH 14 | inserted_protocol AS ( 15 | INSERT INTO 16 | protocols_details ( 17 | protocol_id, 18 | network_id, 19 | deployed_block, 20 | deployed_at 21 | ) 22 | VALUES 23 | ('aave_v3', 'ethereum', 16291127, '2023-01-27') RETURNING id 24 | ) 25 | INSERT INTO 26 | protocols_contracts (protocol_details_id, name, address) 27 | SELECT 28 | id, 29 | name, 30 | address 31 | FROM 32 | inserted_protocol, 33 | ( 34 | VALUES 35 | ( 36 | 'Pool', 37 | '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2' 38 | ), 39 | ( 40 | 'PoolAddressesProvider', 41 | '0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e' 42 | ), 43 | ( 44 | 'UiPoolDataProviderV3', 45 | '0x3F78BBD206e4D3c504Eb854232EdA7e47E9Fd8FC' 46 | ) 47 | ) AS contracts (name, address); 48 | 49 | --- uniswap 50 | INSERT INTO 51 | protocols (id, name, kind) 52 | VALUES 53 | ('uniswap_v3', 'Uniswap V3', 'dex'); 54 | 55 | -------------- ethereum -------------- 56 | WITH 57 | inserted_protocol AS ( 58 | INSERT INTO 59 | protocols_details ( 60 | protocol_id, 61 | network_id, 62 | deployed_block, 63 | deployed_at 64 | ) 65 | VALUES 66 | ('uniswap_v3', 'ethereum', null, null) RETURNING id 67 | ) 68 | INSERT INTO 69 | protocols_contracts (protocol_details_id, name, address) 70 | SELECT 71 | id, 72 | name, 73 | address 74 | FROM 75 | inserted_protocol, 76 | ( 77 | VALUES 78 | ( 79 | 'UniswapV3Factory', 80 | '0x1F98431c8aD98523631AE4a59f267346ea31F984' 81 | ), 82 | ( 83 | 'QuoterV2', 84 | '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' 85 | ) 86 | ) AS contracts (name, address); 87 | -------------------------------------------------------------------------------- /abis/aave_v3/Pool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "admin", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "implementation", 20 | "type": "address" 21 | } 22 | ], 23 | "name": "Upgraded", 24 | "type": "event" 25 | }, 26 | { 27 | "stateMutability": "payable", 28 | "type": "fallback" 29 | }, 30 | { 31 | "inputs": [], 32 | "name": "admin", 33 | "outputs": [ 34 | { 35 | "internalType": "address", 36 | "name": "", 37 | "type": "address" 38 | } 39 | ], 40 | "stateMutability": "nonpayable", 41 | "type": "function" 42 | }, 43 | { 44 | "inputs": [], 45 | "name": "implementation", 46 | "outputs": [ 47 | { 48 | "internalType": "address", 49 | "name": "", 50 | "type": "address" 51 | } 52 | ], 53 | "stateMutability": "nonpayable", 54 | "type": "function" 55 | }, 56 | { 57 | "inputs": [ 58 | { 59 | "internalType": "address", 60 | "name": "_logic", 61 | "type": "address" 62 | }, 63 | { 64 | "internalType": "bytes", 65 | "name": "_data", 66 | "type": "bytes" 67 | } 68 | ], 69 | "name": "initialize", 70 | "outputs": [], 71 | "stateMutability": "payable", 72 | "type": "function" 73 | }, 74 | { 75 | "inputs": [ 76 | { 77 | "internalType": "address", 78 | "name": "newImplementation", 79 | "type": "address" 80 | } 81 | ], 82 | "name": "upgradeTo", 83 | "outputs": [], 84 | "stateMutability": "nonpayable", 85 | "type": "function" 86 | }, 87 | { 88 | "inputs": [ 89 | { 90 | "internalType": "address", 91 | "name": "newImplementation", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "bytes", 96 | "name": "data", 97 | "type": "bytes" 98 | } 99 | ], 100 | "name": "upgradeToAndCall", 101 | "outputs": [], 102 | "stateMutability": "payable", 103 | "type": "function" 104 | } 105 | ] 106 | -------------------------------------------------------------------------------- /abis/chainlink/CLSynchronicityPriceAdapterPegToBase.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "pegToBaseAggregatorAddress", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "assetToPegAggregatorAddress", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint8", 16 | "name": "decimals", 17 | "type": "uint8" 18 | } 19 | ], 20 | "stateMutability": "nonpayable", 21 | "type": "constructor" 22 | }, 23 | { 24 | "inputs": [], 25 | "name": "DecimalsAboveLimit", 26 | "type": "error" 27 | }, 28 | { 29 | "inputs": [], 30 | "name": "DecimalsNotEqual", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [], 35 | "name": "ASSET_TO_PEG", 36 | "outputs": [ 37 | { 38 | "internalType": "contract IChainlinkAggregator", 39 | "name": "", 40 | "type": "address" 41 | } 42 | ], 43 | "stateMutability": "view", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": [], 48 | "name": "DECIMALS", 49 | "outputs": [ 50 | { 51 | "internalType": "uint8", 52 | "name": "", 53 | "type": "uint8" 54 | } 55 | ], 56 | "stateMutability": "view", 57 | "type": "function" 58 | }, 59 | { 60 | "inputs": [], 61 | "name": "DENOMINATOR", 62 | "outputs": [ 63 | { 64 | "internalType": "int256", 65 | "name": "", 66 | "type": "int256" 67 | } 68 | ], 69 | "stateMutability": "view", 70 | "type": "function" 71 | }, 72 | { 73 | "inputs": [], 74 | "name": "MAX_DECIMALS", 75 | "outputs": [ 76 | { 77 | "internalType": "uint8", 78 | "name": "", 79 | "type": "uint8" 80 | } 81 | ], 82 | "stateMutability": "view", 83 | "type": "function" 84 | }, 85 | { 86 | "inputs": [], 87 | "name": "PEG_TO_BASE", 88 | "outputs": [ 89 | { 90 | "internalType": "contract IChainlinkAggregator", 91 | "name": "", 92 | "type": "address" 93 | } 94 | ], 95 | "stateMutability": "view", 96 | "type": "function" 97 | }, 98 | { 99 | "inputs": [], 100 | "name": "latestAnswer", 101 | "outputs": [ 102 | { 103 | "internalType": "int256", 104 | "name": "", 105 | "type": "int256" 106 | } 107 | ], 108 | "stateMutability": "view", 109 | "type": "function" 110 | } 111 | ] -------------------------------------------------------------------------------- /src/contracts.rs: -------------------------------------------------------------------------------- 1 | use alloy::sol_types::sol; 2 | 3 | pub mod liquidator { 4 | use super::sol; 5 | 6 | sol! { 7 | #[sol(rpc)] 8 | #[derive(Debug)] 9 | LiquidatoorContract, 10 | "./abis/Liquidator.json" 11 | } 12 | } 13 | 14 | pub mod aave_v3 { 15 | use serde::{Deserialize, Serialize}; 16 | 17 | use super::sol; 18 | 19 | sol! { 20 | #[allow(clippy::too_many_arguments)] 21 | #[sol(rpc)] 22 | #[derive(Debug, Deserialize, Serialize)] 23 | PoolContract, 24 | "./abis/aave_v3/PoolInstance.json" 25 | } 26 | 27 | sol! { 28 | #[sol(rpc)] 29 | #[derive(Debug)] 30 | AddressProviderContract, 31 | "./abis/aave_v3/PoolAddressesProvider.json" 32 | } 33 | 34 | // workaround of err 35 | // `previous definition of the module `DataTypes`` 36 | mod tmp { 37 | use super::sol; 38 | 39 | sol! { 40 | #[sol(rpc)] 41 | #[derive(Debug)] 42 | DataProviderContract, 43 | "./abis/aave_v3/UIPoolDataProviderV3.json" 44 | } 45 | } 46 | 47 | pub use tmp::DataProviderContract; 48 | } 49 | 50 | pub mod chainlink { 51 | use super::sol; 52 | 53 | sol! { 54 | #[sol(rpc)] 55 | #[derive(Debug)] 56 | CLRatePriceCapAdapterContract, 57 | "./abis/chainlink/CLRatePriceCapAdapter.json" 58 | } 59 | 60 | sol! { 61 | #[sol(rpc)] 62 | #[derive(Debug)] 63 | CLSynchronicityPriceAdapterPegToBaseContract, 64 | "./abis/chainlink/CLSynchronicityPriceAdapterPegToBase.json" 65 | } 66 | 67 | sol! { 68 | #[sol(rpc)] 69 | #[derive(Debug)] 70 | PriceCapAdapterStableContract, 71 | "./abis/chainlink/PriceCapAdapterStable.json" 72 | } 73 | 74 | sol! { 75 | #[sol(rpc)] 76 | #[derive(Debug)] 77 | EACAggregatorProxyContract, 78 | "./abis/chainlink/EACAggregatorProxy.json" 79 | } 80 | 81 | sol! { 82 | #[sol(rpc)] 83 | #[derive(Debug)] 84 | OffchainAggregatorContract, 85 | "./abis/chainlink/AccessControlledOffchainAggregator.json" 86 | } 87 | } 88 | 89 | pub mod uniswap_v3 { 90 | use super::sol; 91 | 92 | sol! { 93 | #[allow(missing_docs)] 94 | #[sol(rpc)] 95 | #[derive(Debug)] 96 | QuoterContract, 97 | "./abis/uniswap_v3/QuoterV2.json" 98 | } 99 | 100 | sol! { 101 | #[allow(missing_docs)] 102 | #[sol(rpc)] 103 | #[derive(Debug)] 104 | FactoryContract, 105 | "./abis/uniswap_v3/UniswapV3Factory.json" 106 | } 107 | 108 | sol! { 109 | #[sol(rpc)] 110 | #[derive(Debug)] 111 | PoolContract, 112 | "./abis/uniswap_v3/UniswapV3Pool.json" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use actix::prelude::*; 4 | use alloy::{ 5 | network::EthereumWallet, 6 | providers::{ProviderBuilder, WsConnect}, 7 | signers::local::PrivateKeySigner, 8 | }; 9 | use sqlx::postgres::PgPoolOptions; 10 | use tokio::signal::unix::{signal, SignalKind}; 11 | 12 | use crate::{ 13 | actors::{Database, Executor, Fanatic, Follower}, 14 | configs::{Config, DatabaseConfig, ExecutorConfig, FanaticConfig, FollowerConfig}, 15 | }; 16 | 17 | #[derive(Message)] 18 | #[rtype(result = "eyre::Result<()>")] 19 | pub struct Shutdown; 20 | 21 | pub async fn run(config: Config) { 22 | let mut interrupt = 23 | signal(SignalKind::interrupt()).expect("Unable to initialise interrupt signal handler"); 24 | let mut terminate = 25 | signal(SignalKind::terminate()).expect("Unable to initialise termination signal handler"); 26 | 27 | /* Init any clients, db conns etc. */ 28 | let (provider_with_wallet, db_pool) = { 29 | ( 30 | ProviderBuilder::new() 31 | .wallet(EthereumWallet::from( 32 | config.account_privkey.parse::().unwrap(), 33 | )) 34 | .on_ws(WsConnect::new(config.ws_url)) 35 | .await 36 | .expect("Unable to initialise provider with wallet"), 37 | Arc::new( 38 | PgPoolOptions::new() 39 | .connect(&config.database_url) 40 | .await 41 | .expect("Unable to establish database connection"), 42 | ), 43 | ) 44 | }; 45 | 46 | /* Spin up the database actor */ 47 | let db_addr = Database::new(DatabaseConfig { 48 | pool: db_pool.clone(), 49 | }) 50 | .await 51 | .start(); 52 | 53 | /* Spin up the follower actor */ 54 | let follower_addr = Follower::new(FollowerConfig { 55 | provider: provider_with_wallet.clone(), 56 | db_addr: db_addr.clone(), 57 | target: config.target.clone(), 58 | }) 59 | .await 60 | .expect("Unable to initialise follower actor") 61 | .start(); 62 | 63 | /* Spin up the fanatic actor */ 64 | let fanatic_addr = Fanatic::new(FanaticConfig { 65 | provider: provider_with_wallet.clone(), 66 | db_addr: db_addr.clone(), 67 | follower_addr: follower_addr.clone(), 68 | target: config.target.clone(), 69 | }) 70 | .await 71 | .expect("Unable to initialise Fanatic actor") 72 | .init() 73 | .await 74 | .expect("Unable to initialise Fanatic actor") 75 | .start(); 76 | 77 | /* Spin up the alpha executor actor */ 78 | let _ = Executor::new(ExecutorConfig { 79 | provider: provider_with_wallet.clone(), 80 | db_addr: db_addr.clone(), 81 | fanatic_addr: fanatic_addr.clone(), 82 | bot_addr: config.bot_addr, 83 | target: config.target, 84 | }) 85 | .await 86 | .expect("Unable to initialise Executor actor") 87 | .start(); 88 | 89 | tokio::select! { 90 | _ = interrupt.recv() => { 91 | fanatic_addr.send(Shutdown).await.unwrap().unwrap(); 92 | System::current().stop(); 93 | } 94 | _ = terminate.recv() => { 95 | fanatic_addr.send(Shutdown).await.unwrap().unwrap(); 96 | System::current().stop(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /abis/chainlink/PriceCapAdapterStable.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "components": [ 6 | { 7 | "internalType": "contract IACLManager", 8 | "name": "aclManager", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "contract IChainlinkAggregator", 13 | "name": "assetToUsdAggregator", 14 | "type": "address" 15 | }, 16 | { 17 | "internalType": "string", 18 | "name": "adapterDescription", 19 | "type": "string" 20 | }, 21 | { 22 | "internalType": "int256", 23 | "name": "priceCap", 24 | "type": "int256" 25 | } 26 | ], 27 | "internalType": "struct IPriceCapAdapterStable.CapAdapterStableParams", 28 | "name": "capAdapterStableParams", 29 | "type": "tuple" 30 | } 31 | ], 32 | "stateMutability": "nonpayable", 33 | "type": "constructor" 34 | }, 35 | { 36 | "inputs": [], 37 | "name": "ACLManagerIsZeroAddress", 38 | "type": "error" 39 | }, 40 | { 41 | "inputs": [], 42 | "name": "CallerIsNotRiskOrPoolAdmin", 43 | "type": "error" 44 | }, 45 | { 46 | "inputs": [], 47 | "name": "CapLowerThanActualPrice", 48 | "type": "error" 49 | }, 50 | { 51 | "inputs": [], 52 | "name": "DecimalsAboveLimit", 53 | "type": "error" 54 | }, 55 | { 56 | "inputs": [], 57 | "name": "DecimalsNotEqual", 58 | "type": "error" 59 | }, 60 | { 61 | "inputs": [], 62 | "name": "RatioOutOfBounds", 63 | "type": "error" 64 | }, 65 | { 66 | "anonymous": false, 67 | "inputs": [ 68 | { 69 | "indexed": false, 70 | "internalType": "int256", 71 | "name": "priceCap", 72 | "type": "int256" 73 | } 74 | ], 75 | "name": "PriceCapUpdated", 76 | "type": "event" 77 | }, 78 | { 79 | "inputs": [], 80 | "name": "ACL_MANAGER", 81 | "outputs": [ 82 | { 83 | "internalType": "contract IACLManager", 84 | "name": "", 85 | "type": "address" 86 | } 87 | ], 88 | "stateMutability": "view", 89 | "type": "function" 90 | }, 91 | { 92 | "inputs": [], 93 | "name": "ASSET_TO_USD_AGGREGATOR", 94 | "outputs": [ 95 | { 96 | "internalType": "contract IChainlinkAggregator", 97 | "name": "", 98 | "type": "address" 99 | } 100 | ], 101 | "stateMutability": "view", 102 | "type": "function" 103 | }, 104 | { 105 | "inputs": [], 106 | "name": "decimals", 107 | "outputs": [ 108 | { 109 | "internalType": "uint8", 110 | "name": "", 111 | "type": "uint8" 112 | } 113 | ], 114 | "stateMutability": "view", 115 | "type": "function" 116 | }, 117 | { 118 | "inputs": [], 119 | "name": "description", 120 | "outputs": [ 121 | { 122 | "internalType": "string", 123 | "name": "", 124 | "type": "string" 125 | } 126 | ], 127 | "stateMutability": "view", 128 | "type": "function" 129 | }, 130 | { 131 | "inputs": [], 132 | "name": "getPriceCap", 133 | "outputs": [ 134 | { 135 | "internalType": "int256", 136 | "name": "", 137 | "type": "int256" 138 | } 139 | ], 140 | "stateMutability": "view", 141 | "type": "function" 142 | }, 143 | { 144 | "inputs": [], 145 | "name": "isCapped", 146 | "outputs": [ 147 | { 148 | "internalType": "bool", 149 | "name": "", 150 | "type": "bool" 151 | } 152 | ], 153 | "stateMutability": "view", 154 | "type": "function" 155 | }, 156 | { 157 | "inputs": [], 158 | "name": "latestAnswer", 159 | "outputs": [ 160 | { 161 | "internalType": "int256", 162 | "name": "", 163 | "type": "int256" 164 | } 165 | ], 166 | "stateMutability": "view", 167 | "type": "function" 168 | }, 169 | { 170 | "inputs": [ 171 | { 172 | "internalType": "int256", 173 | "name": "priceCap", 174 | "type": "int256" 175 | } 176 | ], 177 | "name": "setPriceCap", 178 | "outputs": [], 179 | "stateMutability": "nonpayable", 180 | "type": "function" 181 | } 182 | ] -------------------------------------------------------------------------------- /migrations/20240101010101_create_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS networks ( 2 | id VARCHAR(50) PRIMARY KEY, 3 | chain_id INTEGER NULL, 4 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 5 | ); 6 | 7 | CREATE TABLE IF NOT EXISTS protocols ( 8 | id VARCHAR(50) PRIMARY KEY, 9 | name VARCHAR(100) NOT NULL, 10 | kind VARCHAR(50) NOT NULL, 11 | fork VARCHAR(50) REFERENCES protocols (id), 12 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS protocols_details ( 16 | id SERIAL PRIMARY KEY, 17 | protocol_id VARCHAR(50) NOT NULL REFERENCES protocols (id), 18 | network_id VARCHAR(50) NOT NULL REFERENCES networks (id), 19 | deployed_block BIGINT, 20 | deployed_at TIMESTAMP, 21 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 22 | UNIQUE (protocol_id, network_id) 23 | ); 24 | 25 | CREATE TABLE IF NOT EXISTS protocols_contracts ( 26 | id SERIAL PRIMARY KEY, 27 | protocol_details_id INTEGER NOT NULL REFERENCES protocols_details (id), 28 | name VARCHAR(100) NOT NULL, 29 | address CHAR(42) NOT NULL, 30 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 31 | UNIQUE (protocol_details_id, address) 32 | ); 33 | 34 | CREATE TABLE IF NOT EXISTS erc20 ( 35 | id SERIAL PRIMARY KEY, 36 | symbol VARCHAR(10) NOT NULL, 37 | name VARCHAR(100) NOT NULL, 38 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 39 | ); 40 | 41 | CREATE TABLE IF NOT EXISTS erc20_details ( 42 | id SERIAL PRIMARY KEY, 43 | address CHAR(42) NOT NULL UNIQUE, 44 | decimals INTEGER NOT NULL, 45 | erc20_id INTEGER NOT NULL REFERENCES erc20 (id), 46 | network_id CHAR(50) NOT NULL REFERENCES networks (id), 47 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 48 | UNIQUE (network_id, address) 49 | ); 50 | 51 | CREATE TABLE IF NOT EXISTS aavev3_users ( 52 | address CHAR(42) PRIMARY KEY, 53 | protocol_details_id INTEGER REFERENCES protocols_details (id), 54 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 55 | ); 56 | 57 | CREATE TABLE IF NOT EXISTS aavev3_users_stats ( 58 | user_address CHAR(42) REFERENCES aavev3_users (address), 59 | health_factor DOUBLE PRECISION NOT NULL, 60 | updated_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 61 | PRIMARY KEY (user_address) 62 | ); 63 | 64 | CREATE TABLE IF NOT EXISTS aavev3_reserves ( 65 | reserve CHAR(42) PRIMARY KEY REFERENCES erc20_details (address), 66 | protocol_details_id INTEGER NOT NULL REFERENCES protocols_details (id), 67 | liquidation_threshold DOUBLE PRECISION NOT NULL, 68 | liquidation_bonus DOUBLE PRECISION NOT NULL, 69 | flashloan_enabled BOOLEAN NOT NULL, 70 | oracle_addr CHAR(42) NOT NULL, 71 | -- some reserves don't have aggregators, i.e GHO.. https://etherscan.io/address/0xd110cac5d8682a3b045d5524a9903e031d70fccd 72 | aggregator_addr CHAR(42), 73 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 74 | ); 75 | 76 | CREATE TABLE IF NOT EXISTS aavev3_reserves_stats ( 77 | reserve CHAR(42) NOT NULL REFERENCES aavev3_reserves (reserve), 78 | liquidity_index DOUBLE PRECISION NOT NULL, 79 | liquidity_rate DOUBLE PRECISION NOT NULL, 80 | variable_borrow_rate DOUBLE PRECISION NOT NULL, 81 | variable_borrow_index DOUBLE PRECISION NOT NULL, 82 | price_usd DOUBLE PRECISION, 83 | updated_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 84 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 85 | PRIMARY KEY (reserve) 86 | ); 87 | 88 | CREATE TABLE IF NOT EXISTS aavev3_positions ( 89 | id SERIAL PRIMARY KEY, 90 | user_address CHAR(42) NOT NULL REFERENCES aavev3_users (address), 91 | reserve CHAR(42) NOT NULL REFERENCES aavev3_reserves (reserve), 92 | supply_amount DOUBLE PRECISION NOT NULL DEFAULT 0, 93 | borrow_amount DOUBLE PRECISION NOT NULL DEFAULT 0, 94 | updated_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 95 | created_at TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC'), 96 | UNIQUE (user_address, reserve) 97 | ); 98 | 99 | CREATE TABLE IF NOT EXISTS aavev3_liquidations ( 100 | id SERIAL PRIMARY KEY, 101 | protocol_details_id INTEGER REFERENCES protocols_details (id), 102 | -- "user" is a reserved keyword in SQL 103 | --user_address CHAR(42) REFERENCES aavev3_users (address), 104 | user_address CHAR(42) NOT NULL, 105 | --liquidator_address CHAR(42) NOT NULL, 106 | collateral_asset CHAR(42) NOT NULL REFERENCES aavev3_reserves (reserve), 107 | debt_asset CHAR(42) NOT NULL REFERENCES aavev3_reserves (reserve), 108 | timestamp TIMESTAMP NOT NULL DEFAULT (NOW () AT TIME ZONE 'UTC') 109 | --missed BOOLEAN NOT NULL DEFAULT TRUE 110 | ); 111 | -------------------------------------------------------------------------------- /contracts/src/Liquidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "@forge-std/console.sol"; 5 | 6 | import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; 7 | import {Ownable} from "@openzeppelin/access/Ownable.sol"; 8 | import {IFlashLoanSimpleReceiver} from "@aave-v3/misc/flashloan/interfaces/IFlashLoanSimpleReceiver.sol"; 9 | import {FlashLoanSimpleReceiverBase} from "@aave-v3/misc/flashloan/base/FlashLoanSimpleReceiverBase.sol"; 10 | import {IPoolAddressesProvider} from "@aave-v3/interfaces/IPoolAddressesProvider.sol"; 11 | import {ISwapRouter} from "@uniswap-v3-periphery/interfaces/ISwapRouter.sol"; 12 | 13 | contract Liquidatoor is FlashLoanSimpleReceiverBase, Ownable { 14 | ISwapRouter public immutable swapRouter; 15 | 16 | receive() external payable {} 17 | 18 | constructor( 19 | IPoolAddressesProvider _addressProvider, 20 | ISwapRouter _swapRouter 21 | ) FlashLoanSimpleReceiverBase(_addressProvider) Ownable(msg.sender) { 22 | swapRouter = _swapRouter; 23 | } 24 | 25 | /* 26 | * @notice Withdraws ETH from the contract. 27 | */ 28 | function withdrawalETH() external onlyOwner { 29 | (bool success, ) = payable(msg.sender).call{ 30 | value: address(this).balance 31 | }(""); 32 | require(success, "ETH transfer failed"); 33 | } 34 | 35 | /* 36 | * @notice Withdraws ERC20 tokens from the contract. 37 | */ 38 | function withdrawalERC20(address token) external onlyOwner { 39 | IERC20(token).transfer( 40 | msg.sender, 41 | IERC20(token).balanceOf(address(this)) 42 | ); 43 | } 44 | 45 | /* 46 | * @notice Called by the Aave Pool after your contract has received the flashloan. 47 | */ 48 | function executeOperation( 49 | address asset, 50 | uint256 amount, 51 | uint256 premium, 52 | address initiator, 53 | bytes calldata params 54 | ) external override returns (bool) { 55 | (address collateral_asset, address user, uint24 fee) = abi.decode( 56 | params, 57 | (address, address, uint24) 58 | ); 59 | 60 | IERC20(asset).approve(address(POOL), amount); 61 | uint256 collateralBalance = IERC20(collateral_asset).balanceOf( 62 | address(this) 63 | ); 64 | 65 | POOL.liquidationCall( 66 | collateral_asset, 67 | asset, // debt asset 68 | user, 69 | amount, 70 | false 71 | ); 72 | 73 | if (collateral_asset != asset) { 74 | collateralBalance = IERC20(collateral_asset).balanceOf( 75 | address(this) 76 | ); 77 | 78 | // approve the Uniswap router to spend the collateral. 79 | IERC20(collateral_asset).approve( 80 | address(swapRouter), 81 | collateralBalance 82 | ); 83 | IERC20(collateral_asset).allowance( 84 | address(this), 85 | address(swapRouter) 86 | ); 87 | 88 | // setup swap parameters 89 | ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter 90 | .ExactInputSingleParams({ 91 | tokenIn: collateral_asset, // ETH 92 | tokenOut: asset, // USDT 93 | fee: fee, 94 | recipient: address(this), 95 | deadline: block.timestamp + 10, 96 | amountIn: collateralBalance, 97 | amountOutMinimum: 0 98 | sqrtPriceLimitX96: 0 99 | }); 100 | 101 | swapRouter.exactInputSingle(swapParams); 102 | } 103 | 104 | uint256 totalRepayment = amount + premium; 105 | // Approve the pool to pull the flash loan repayment 106 | IERC20(asset).approve(address(POOL), totalRepayment); 107 | 108 | return true; 109 | } 110 | 111 | /* 112 | * @notice Initiates a flashloan to liquidate an undercollateralized position. 113 | * @param asset The debt asset to be repaid through the flashloan. 114 | * @param collateral The collateral asset to be liquidated. 115 | * @param userToLiquidate The address of the user to liquidate. 116 | * @param amount The amount to liquidate. 117 | */ 118 | function liquidatoor( 119 | address debt_asset, 120 | address collateral_asset, 121 | address userToLiquidate, 122 | uint256 amount, 123 | uint24 fee 124 | ) external onlyOwner { 125 | // pack collateral and user for use in executeOperation 126 | bytes memory params = abi.encode( 127 | collateral_asset, 128 | userToLiquidate, 129 | fee 130 | ); 131 | 132 | // Initiate the flashLoan with the updated parameters 133 | POOL.flashLoanSimple( 134 | address(this), 135 | debt_asset, 136 | amount, 137 | params, 138 | 0 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::contracts; 4 | use alloy::{ 5 | primitives::{utils::format_ether, Address, Uint, U256}, 6 | providers::Provider, 7 | }; 8 | use tracing::info; 9 | 10 | // Liquidators can only close a certain amount of collateral defined by a close factor. 11 | // Currently the close factor is 0.5. In other words, 12 | // liquidators can only liquidate a maximum of 50% of the amount pending to be repaid in a position. 13 | // The liquidation discount applies to this amount. 14 | pub const CLOSE_FACTOR: f64 = 0.5; 15 | 16 | pub fn norm(val: T, factor: Option) -> eyre::Result 17 | where 18 | T: ToString, 19 | { 20 | Ok(val.to_string().parse::()? * factor.unwrap_or(1.0)) 21 | } 22 | 23 | pub async fn health_factor( 24 | contract: &contracts::aave_v3::PoolContract::PoolContractInstance<(), P>, 25 | user: Address, 26 | ) -> Option { 27 | let data = contract.getUserAccountData(user).call().await.unwrap(); 28 | let health_factor = format_ether(data.healthFactor).parse::().unwrap(); 29 | 30 | // sanity check — ensure health factor is within a reasonable range 31 | if health_factor > 10_000.0 { 32 | return None; 33 | } 34 | 35 | Some(health_factor) 36 | } 37 | 38 | pub async fn user_liquidation_data( 39 | datap_contract: &contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 40 | provider_addr: Address, 41 | user: Address, 42 | indices: &HashMap, 43 | ) -> eyre::Result<(Address, Address, U256)> { 44 | let user_reserves = datap_contract 45 | .getUserReservesData(provider_addr, user) 46 | .call() 47 | .await?; 48 | 49 | let debt = user_reserves 50 | ._0 51 | .iter() 52 | .find(|v| v.scaledVariableDebt > U256::from(0)) 53 | .ok_or(eyre::eyre!("No debt asset found"))?; 54 | 55 | let (_, var_idx) = indices 56 | .get(&debt.underlyingAsset.to_string()) 57 | .unwrap_or(&(1.0, 1.0)); 58 | let normalized_debt = norm(debt.scaledVariableDebt, Some(*var_idx))?; 59 | let debt_to_cover = debt_to_cover(U256::from(normalized_debt)); 60 | 61 | let collateral = user_reserves 62 | ._0 63 | .iter() 64 | .find(|v| v.scaledATokenBalance > U256::from(0)) 65 | .ok_or(eyre::eyre!("No collateral asset found"))?; 66 | 67 | info!( 68 | debt_asset = ?debt.underlyingAsset, 69 | debt = ?debt.scaledVariableDebt, 70 | collateral = ?collateral.scaledATokenBalance, 71 | collateral_asset = ?collateral.underlyingAsset, 72 | debt_to_cover = ?debt_to_cover, 73 | ); 74 | 75 | let debt_asset = debt.underlyingAsset; 76 | let collateral_asset = collateral.underlyingAsset; 77 | 78 | Ok((debt_asset, collateral_asset, debt_to_cover)) 79 | } 80 | 81 | pub fn debt_to_cover(variable_debt: U256) -> U256 { 82 | variable_debt * Uint::from(CLOSE_FACTOR) 83 | } 84 | 85 | pub async fn find_most_liquid_uniswap_pool( 86 | provider: &P, 87 | factory_contract: &contracts::uniswap_v3::FactoryContract::FactoryContractInstance<(), P>, 88 | collateral_asset: Address, 89 | debt_asset: Address, 90 | ) -> eyre::Result<(Address, u16)> { 91 | const FEES: [u16; 3] = [500, 3000, 10000]; 92 | 93 | let mut max_liquidity = U256::ZERO; 94 | let mut best_pool = None; 95 | 96 | for fee in FEES { 97 | let pool = factory_contract 98 | .getPool(collateral_asset, debt_asset, Uint::from(fee)) 99 | .call() 100 | .await?; 101 | 102 | if pool._0 != Address::ZERO { 103 | let contract = contracts::uniswap_v3::PoolContract::new(pool._0, provider.clone()); 104 | let liquidity = U256::from(contract.liquidity().call().await?._0); 105 | 106 | if liquidity > max_liquidity { 107 | max_liquidity = liquidity; 108 | best_pool = Some((pool._0, fee)); 109 | } 110 | } 111 | } 112 | 113 | info!(?best_pool, "most liquid uniswap_v3 pool"); 114 | best_pool.ok_or(eyre::eyre!("No valid pool found")) 115 | } 116 | 117 | pub async fn user_positions( 118 | datap_contract: &contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 119 | addressp_addr: &Address, 120 | user: &Address, 121 | indices: &HashMap, 122 | ) -> eyre::Result> { 123 | let user_data = datap_contract 124 | .getUserReservesData(*addressp_addr, *user) 125 | .call() 126 | .await?; 127 | 128 | Ok(user_data 129 | ._0 130 | .iter() 131 | .filter(|r| !r.scaledATokenBalance.is_zero() || !r.scaledVariableDebt.is_zero()) 132 | .map(|r| { 133 | let addr = r.underlyingAsset; 134 | let (liq_idx, var_idx) = indices.get(&addr.to_string()).unwrap_or(&(1.0, 1.0)); 135 | let collateral = 136 | norm(r.scaledATokenBalance, Some(*liq_idx)).expect("supply calc failed"); 137 | let debt = norm(r.scaledVariableDebt, Some(*var_idx)).expect("borrow calc failed"); 138 | (addr, collateral, debt) 139 | }) 140 | .collect()) 141 | } 142 | -------------------------------------------------------------------------------- /abis/uniswap_v3/UniswapV3Factory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "uint24", 13 | "name": "fee", 14 | "type": "uint24" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "int24", 19 | "name": "tickSpacing", 20 | "type": "int24" 21 | } 22 | ], 23 | "name": "FeeAmountEnabled", 24 | "type": "event" 25 | }, 26 | { 27 | "anonymous": false, 28 | "inputs": [ 29 | { 30 | "indexed": true, 31 | "internalType": "address", 32 | "name": "oldOwner", 33 | "type": "address" 34 | }, 35 | { 36 | "indexed": true, 37 | "internalType": "address", 38 | "name": "newOwner", 39 | "type": "address" 40 | } 41 | ], 42 | "name": "OwnerChanged", 43 | "type": "event" 44 | }, 45 | { 46 | "anonymous": false, 47 | "inputs": [ 48 | { 49 | "indexed": true, 50 | "internalType": "address", 51 | "name": "token0", 52 | "type": "address" 53 | }, 54 | { 55 | "indexed": true, 56 | "internalType": "address", 57 | "name": "token1", 58 | "type": "address" 59 | }, 60 | { 61 | "indexed": true, 62 | "internalType": "uint24", 63 | "name": "fee", 64 | "type": "uint24" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "int24", 69 | "name": "tickSpacing", 70 | "type": "int24" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "address", 75 | "name": "pool", 76 | "type": "address" 77 | } 78 | ], 79 | "name": "PoolCreated", 80 | "type": "event" 81 | }, 82 | { 83 | "inputs": [ 84 | { 85 | "internalType": "address", 86 | "name": "tokenA", 87 | "type": "address" 88 | }, 89 | { 90 | "internalType": "address", 91 | "name": "tokenB", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "uint24", 96 | "name": "fee", 97 | "type": "uint24" 98 | } 99 | ], 100 | "name": "createPool", 101 | "outputs": [ 102 | { 103 | "internalType": "address", 104 | "name": "pool", 105 | "type": "address" 106 | } 107 | ], 108 | "stateMutability": "nonpayable", 109 | "type": "function" 110 | }, 111 | { 112 | "inputs": [ 113 | { 114 | "internalType": "uint24", 115 | "name": "fee", 116 | "type": "uint24" 117 | }, 118 | { 119 | "internalType": "int24", 120 | "name": "tickSpacing", 121 | "type": "int24" 122 | } 123 | ], 124 | "name": "enableFeeAmount", 125 | "outputs": [], 126 | "stateMutability": "nonpayable", 127 | "type": "function" 128 | }, 129 | { 130 | "inputs": [ 131 | { 132 | "internalType": "uint24", 133 | "name": "", 134 | "type": "uint24" 135 | } 136 | ], 137 | "name": "feeAmountTickSpacing", 138 | "outputs": [ 139 | { 140 | "internalType": "int24", 141 | "name": "", 142 | "type": "int24" 143 | } 144 | ], 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "inputs": [ 150 | { 151 | "internalType": "address", 152 | "name": "", 153 | "type": "address" 154 | }, 155 | { 156 | "internalType": "address", 157 | "name": "", 158 | "type": "address" 159 | }, 160 | { 161 | "internalType": "uint24", 162 | "name": "", 163 | "type": "uint24" 164 | } 165 | ], 166 | "name": "getPool", 167 | "outputs": [ 168 | { 169 | "internalType": "address", 170 | "name": "", 171 | "type": "address" 172 | } 173 | ], 174 | "stateMutability": "view", 175 | "type": "function" 176 | }, 177 | { 178 | "inputs": [], 179 | "name": "owner", 180 | "outputs": [ 181 | { 182 | "internalType": "address", 183 | "name": "", 184 | "type": "address" 185 | } 186 | ], 187 | "stateMutability": "view", 188 | "type": "function" 189 | }, 190 | { 191 | "inputs": [], 192 | "name": "parameters", 193 | "outputs": [ 194 | { 195 | "internalType": "address", 196 | "name": "factory", 197 | "type": "address" 198 | }, 199 | { 200 | "internalType": "address", 201 | "name": "token0", 202 | "type": "address" 203 | }, 204 | { 205 | "internalType": "address", 206 | "name": "token1", 207 | "type": "address" 208 | }, 209 | { 210 | "internalType": "uint24", 211 | "name": "fee", 212 | "type": "uint24" 213 | }, 214 | { 215 | "internalType": "int24", 216 | "name": "tickSpacing", 217 | "type": "int24" 218 | } 219 | ], 220 | "stateMutability": "view", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { 226 | "internalType": "address", 227 | "name": "_owner", 228 | "type": "address" 229 | } 230 | ], 231 | "name": "setOwner", 232 | "outputs": [], 233 | "stateMutability": "nonpayable", 234 | "type": "function" 235 | } 236 | ] 237 | -------------------------------------------------------------------------------- /abis/uniswap_v3/QuoterV2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_factory", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_WETH9", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "inputs": [], 20 | "name": "WETH9", 21 | "outputs": [ 22 | { 23 | "internalType": "address", 24 | "name": "", 25 | "type": "address" 26 | } 27 | ], 28 | "stateMutability": "view", 29 | "type": "function" 30 | }, 31 | { 32 | "inputs": [], 33 | "name": "factory", 34 | "outputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "", 38 | "type": "address" 39 | } 40 | ], 41 | "stateMutability": "view", 42 | "type": "function" 43 | }, 44 | { 45 | "inputs": [ 46 | { 47 | "internalType": "bytes", 48 | "name": "path", 49 | "type": "bytes" 50 | }, 51 | { 52 | "internalType": "uint256", 53 | "name": "amountIn", 54 | "type": "uint256" 55 | } 56 | ], 57 | "name": "quoteExactInput", 58 | "outputs": [ 59 | { 60 | "internalType": "uint256", 61 | "name": "amountOut", 62 | "type": "uint256" 63 | }, 64 | { 65 | "internalType": "uint160[]", 66 | "name": "sqrtPriceX96AfterList", 67 | "type": "uint160[]" 68 | }, 69 | { 70 | "internalType": "uint32[]", 71 | "name": "initializedTicksCrossedList", 72 | "type": "uint32[]" 73 | }, 74 | { 75 | "internalType": "uint256", 76 | "name": "gasEstimate", 77 | "type": "uint256" 78 | } 79 | ], 80 | "stateMutability": "nonpayable", 81 | "type": "function" 82 | }, 83 | { 84 | "inputs": [ 85 | { 86 | "components": [ 87 | { 88 | "internalType": "address", 89 | "name": "tokenIn", 90 | "type": "address" 91 | }, 92 | { 93 | "internalType": "address", 94 | "name": "tokenOut", 95 | "type": "address" 96 | }, 97 | { 98 | "internalType": "uint256", 99 | "name": "amountIn", 100 | "type": "uint256" 101 | }, 102 | { 103 | "internalType": "uint24", 104 | "name": "fee", 105 | "type": "uint24" 106 | }, 107 | { 108 | "internalType": "uint160", 109 | "name": "sqrtPriceLimitX96", 110 | "type": "uint160" 111 | } 112 | ], 113 | "internalType": "struct IQuoterV2.QuoteExactInputSingleParams", 114 | "name": "params", 115 | "type": "tuple" 116 | } 117 | ], 118 | "name": "quoteExactInputSingle", 119 | "outputs": [ 120 | { 121 | "internalType": "uint256", 122 | "name": "amountOut", 123 | "type": "uint256" 124 | }, 125 | { 126 | "internalType": "uint160", 127 | "name": "sqrtPriceX96After", 128 | "type": "uint160" 129 | }, 130 | { 131 | "internalType": "uint32", 132 | "name": "initializedTicksCrossed", 133 | "type": "uint32" 134 | }, 135 | { 136 | "internalType": "uint256", 137 | "name": "gasEstimate", 138 | "type": "uint256" 139 | } 140 | ], 141 | "stateMutability": "nonpayable", 142 | "type": "function" 143 | }, 144 | { 145 | "inputs": [ 146 | { 147 | "internalType": "bytes", 148 | "name": "path", 149 | "type": "bytes" 150 | }, 151 | { 152 | "internalType": "uint256", 153 | "name": "amountOut", 154 | "type": "uint256" 155 | } 156 | ], 157 | "name": "quoteExactOutput", 158 | "outputs": [ 159 | { 160 | "internalType": "uint256", 161 | "name": "amountIn", 162 | "type": "uint256" 163 | }, 164 | { 165 | "internalType": "uint160[]", 166 | "name": "sqrtPriceX96AfterList", 167 | "type": "uint160[]" 168 | }, 169 | { 170 | "internalType": "uint32[]", 171 | "name": "initializedTicksCrossedList", 172 | "type": "uint32[]" 173 | }, 174 | { 175 | "internalType": "uint256", 176 | "name": "gasEstimate", 177 | "type": "uint256" 178 | } 179 | ], 180 | "stateMutability": "nonpayable", 181 | "type": "function" 182 | }, 183 | { 184 | "inputs": [ 185 | { 186 | "components": [ 187 | { 188 | "internalType": "address", 189 | "name": "tokenIn", 190 | "type": "address" 191 | }, 192 | { 193 | "internalType": "address", 194 | "name": "tokenOut", 195 | "type": "address" 196 | }, 197 | { 198 | "internalType": "uint256", 199 | "name": "amount", 200 | "type": "uint256" 201 | }, 202 | { 203 | "internalType": "uint24", 204 | "name": "fee", 205 | "type": "uint24" 206 | }, 207 | { 208 | "internalType": "uint160", 209 | "name": "sqrtPriceLimitX96", 210 | "type": "uint160" 211 | } 212 | ], 213 | "internalType": "struct IQuoterV2.QuoteExactOutputSingleParams", 214 | "name": "params", 215 | "type": "tuple" 216 | } 217 | ], 218 | "name": "quoteExactOutputSingle", 219 | "outputs": [ 220 | { 221 | "internalType": "uint256", 222 | "name": "amountIn", 223 | "type": "uint256" 224 | }, 225 | { 226 | "internalType": "uint160", 227 | "name": "sqrtPriceX96After", 228 | "type": "uint160" 229 | }, 230 | { 231 | "internalType": "uint32", 232 | "name": "initializedTicksCrossed", 233 | "type": "uint32" 234 | }, 235 | { 236 | "internalType": "uint256", 237 | "name": "gasEstimate", 238 | "type": "uint256" 239 | } 240 | ], 241 | "stateMutability": "nonpayable", 242 | "type": "function" 243 | }, 244 | { 245 | "inputs": [ 246 | { 247 | "internalType": "int256", 248 | "name": "amount0Delta", 249 | "type": "int256" 250 | }, 251 | { 252 | "internalType": "int256", 253 | "name": "amount1Delta", 254 | "type": "int256" 255 | }, 256 | { 257 | "internalType": "bytes", 258 | "name": "path", 259 | "type": "bytes" 260 | } 261 | ], 262 | "name": "uniswapV3SwapCallback", 263 | "outputs": [], 264 | "stateMutability": "view", 265 | "type": "function" 266 | } 267 | ] 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Standalone AaveV3 liquidation bot, unlikely to be profitable nowadays. 2 | 3 | ## Components: 4 | 5 | - `contracts/` - contains example contract used to execute liquidations 6 | - `src/` - contains the offchain logic responsible for tracking and managing liquidation opportunities 7 | - `migrations/` - holds the database migrations 8 | 9 | The offchain service is built with actix's [actor model](https://en.wikipedia.org/wiki/Actor_model) in mind. 10 | 11 | The setup is abstracted enough to work on any and all Aave forks, on all EVM-compatible chains, so long as you know the deployment address of the protocol's: 12 | 13 | - `Pool` 14 | - `PoolAddressesProvider` 15 | - `UiPoolDataProviderV3` 16 | 17 | Simply create a new migration file in `migrations/`, copy [20240101010102_insert_val](./migrations/20240101010102_insert_val.sql) and adjust the values accordingly. 18 | 19 | ## Idiosyncrasies 20 | 21 | - all reserves's real time value is tracked by listening for `AnswerUpdated`, emitted by Chainlink's price aggregators. 22 | - users's open positions & exposure is kept both in-memory and in postgres for later usage 23 | - the smart contract executing the liquidation relies on flashloan to execute the liquidation 24 | 25 | # Example usage 26 | 27 | - deploy the contract (perhaps locally through anvil fork `anvil --fork-url https://eth.merkle.io`) 28 | 29 | - start postgres 30 | 31 | ```bash 32 | docker-compose up -d postgres 33 | ``` 34 | 35 | run the offchain service 36 | 37 | ```bash 38 | # the used pubkey & privkey are the default ones provided by anvil. 39 | cargo r -- \ 40 | --network ethereum --protocol aave_v3 \ 41 | --ws-url=wss://eth.merkle.io \ 42 | --account-pubkey=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \ 43 | --account-privkey=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ 44 | --bot-addr=0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5 45 | ``` 46 | 47 | # Flow 48 | 49 | The flow of execution goes like 50 | 51 | ```mermaid 52 | sequenceDiagram 53 | 54 | participant DB as DBActor 55 | participant Fanatic as FanaticActor 56 | participant Follower as FollowerActor 57 | participant Executor as ExecutorActor 58 | 59 | Fanatic ->> DB: GetProtocolDetailsId 60 | Fanatic ->> DB: GetProtocolContracts 61 | Fanatic ->> DB: GetReservesUsers 62 | 63 | Fanatic -> Fanatic: init_reserves() 64 | 65 | Fanatic ->> Follower: SendFanaticAddr 66 | Fanatic ->> Follower: StartListeningForOraclePrices 67 | Fanatic ->> Follower: StartListeningForEvents 68 | 69 | rect rgb(30, 30, 30) 70 | Follower ->> Follower: listen_oracle_prices() 71 | Follower ->> DB: UpdateOraclePrice 72 | Follower ->> Fanatic: UpdateReservePrice 73 | Fanatic ->> Fanatic: verify health factors 74 | Fanatic ->> Fanatic: update in memory state 75 | Fanatic ->> Executor: liquidation request 76 | Executor --> Executor: verify opportunity is profitable 77 | Executor --> Executor: execute liquidation 78 | end 79 | 80 | rect rgb(30, 30, 30) 81 | Follower ->> Follower: listen_events() 82 | rect rgb(40, 40, 40) 83 | Follower ->> Fanatic: DoSmthWithLiquidationCall 84 | Fanatic ->> DB: InsertLiquidation 85 | end 86 | 87 | rect rgb(50, 50, 50) 88 | Follower ->> Fanatic: UpdateReserveUser 89 | Fanatic ->> Fanatic: verify health factor 90 | Fanatic ->> Fanatic: update in memory state 91 | Fanatic ->> Executor: liquidation request 92 | Executor --> Executor: verify opportunity is profitable 93 | Executor --> Executor: execute tx 94 | end 95 | 96 | rect rgb(60, 60, 60) 97 | Follower ->> DB: UpsertReserveStats 98 | end 99 | end 100 | ``` 101 | 102 | # Database schema 103 | 104 | ```mermaid 105 | erDiagram 106 | NETWORKS ||--o{ PROTOCOLS_DETAILS : has 107 | PROTOCOLS ||--o{ PROTOCOLS_DETAILS : has 108 | PROTOCOLS ||--o{ PROTOCOLS : forks 109 | PROTOCOLS_DETAILS ||--o{ PROTOCOLS_CONTRACTS : contains 110 | NETWORKS ||--o{ ERC20_DETAILS : has 111 | ERC20 ||--o{ ERC20_DETAILS : has 112 | PROTOCOLS_DETAILS ||--o{ AAV3_USERS : has 113 | PROTOCOLS_DETAILS ||--o{ AAV3_RESERVES : has 114 | AAV3_USERS ||--o{ AAV3_USERS_STATS : tracks 115 | ERC20_DETAILS ||--o{ AAV3_RESERVES : provides 116 | AAV3_RESERVES ||--o{ AAV3_RESERVES_STATS : tracks 117 | AAV3_USERS ||--o{ AAV3_POSITIONS : has 118 | AAV3_RESERVES ||--o{ AAV3_POSITIONS : uses 119 | PROTOCOLS_DETAILS ||--o{ AAV3_LIQUIDATIONS : records 120 | AAV3_RESERVES ||--o{ AAV3_LIQUIDATIONS : involves 121 | 122 | NETWORKS { 123 | VARCHAR(50) id PK 124 | INTEGER chain_id 125 | TIMESTAMP created_at 126 | } 127 | 128 | PROTOCOLS { 129 | VARCHAR(50) id PK 130 | VARCHAR(100) name 131 | VARCHAR(50) kind 132 | VARCHAR(50) fork FK 133 | TIMESTAMP created_at 134 | } 135 | 136 | PROTOCOLS_DETAILS { 137 | SERIAL id PK 138 | VARCHAR(50) protocol_id FK 139 | VARCHAR(50) network_id FK 140 | BIGINT deployed_block 141 | TIMESTAMP deployed_at 142 | TIMESTAMP created_at 143 | } 144 | 145 | PROTOCOLS_CONTRACTS { 146 | SERIAL id PK 147 | INTEGER protocol_details_id FK 148 | VARCHAR(100) name 149 | CHAR(42) address 150 | TIMESTAMP created_at 151 | } 152 | 153 | ERC20 { 154 | SERIAL id PK 155 | VARCHAR(10) symbol 156 | VARCHAR(100) name 157 | TIMESTAMP created_at 158 | } 159 | 160 | ERC20_DETAILS { 161 | SERIAL id PK 162 | CHAR(42) address 163 | INTEGER decimals 164 | INTEGER erc20_id FK 165 | CHAR(50) network_id FK 166 | TIMESTAMP created_at 167 | } 168 | 169 | AAV3_USERS { 170 | CHAR(42) address PK 171 | INTEGER protocol_details_id FK 172 | TIMESTAMP created_at 173 | } 174 | 175 | AAV3_USERS_STATS { 176 | CHAR(42) user_address PK,FK 177 | DOUBLE_PRECISION health_factor 178 | TIMESTAMP updated_at 179 | } 180 | 181 | AAV3_RESERVES { 182 | CHAR(42) reserve PK,FK 183 | INTEGER protocol_details_id FK 184 | DOUBLE_PRECISION liquidation_threshold 185 | DOUBLE_PRECISION liquidation_bonus 186 | BOOLEAN flashloan_enabled 187 | CHAR(42) oracle_addr 188 | CHAR(42) aggregator_addr 189 | TIMESTAMP created_at 190 | } 191 | 192 | AAV3_RESERVES_STATS { 193 | CHAR(42) reserve PK,FK 194 | DOUBLE_PRECISION liquidity_index 195 | DOUBLE_PRECISION liquidity_rate 196 | DOUBLE_PRECISION variable_borrow_rate 197 | DOUBLE_PRECISION variable_borrow_index 198 | DOUBLE_PRECISION price_usd 199 | TIMESTAMP updated_at 200 | TIMESTAMP created_at 201 | } 202 | 203 | AAV3_POSITIONS { 204 | SERIAL id PK 205 | CHAR(42) user_address FK 206 | CHAR(42) reserve FK 207 | DOUBLE_PRECISION supply_amount 208 | DOUBLE_PRECISION borrow_amount 209 | TIMESTAMP updated_at 210 | TIMESTAMP created_at 211 | } 212 | 213 | AAV3_LIQUIDATIONS { 214 | SERIAL id PK 215 | INTEGER protocol_details_id FK 216 | CHAR(42) user_address 217 | CHAR(42) collateral_asset FK 218 | CHAR(42) debt_asset FK 219 | TIMESTAMP timestamp 220 | } 221 | ``` 222 | -------------------------------------------------------------------------------- /src/actors/executor.rs: -------------------------------------------------------------------------------- 1 | use crate::contracts; 2 | use crate::utils::{find_most_liquid_uniswap_pool, health_factor, user_liquidation_data}; 3 | use actix::prelude::*; 4 | use alloy::{ 5 | primitives::{Address, Uint}, 6 | providers::Provider, 7 | }; 8 | 9 | use tracing::{error, info, warn}; 10 | 11 | use super::messages::executor::LiquidationRequest; 12 | use super::messages::fanatic::SendExecutorAddr; 13 | use super::Database; 14 | use super::Fanatic; 15 | use crate::{ 16 | actors::messages::{ 17 | database, 18 | fanatic::{FailedLiquidation, SuccessfulLiquidation}, 19 | }, 20 | configs::ExecutorConfig, 21 | }; 22 | 23 | // executes the liquidations by triggering the liquidator contract 24 | #[derive(Debug, Clone)] 25 | pub struct Executor { 26 | pub provider: P, 27 | 28 | pub db_addr: Addr, 29 | pub fanatic_addr: Addr>, 30 | pub provider_addr: Address, 31 | 32 | // deployed liquidator bot 33 | pub bot_contract: 34 | contracts::liquidator::LiquidatoorContract::LiquidatoorContractInstance<(), P>, 35 | 36 | // aave_v3 37 | pub pool_contract: contracts::aave_v3::PoolContract::PoolContractInstance<(), P>, 38 | pub datap_contract: 39 | contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 40 | 41 | // uniswap_v3 42 | pub factory_contract: contracts::uniswap_v3::FactoryContract::FactoryContractInstance<(), P>, 43 | pub quoter_contract: contracts::uniswap_v3::QuoterContract::QuoterContractInstance<(), P>, 44 | } 45 | 46 | impl Actor for Executor

{ 47 | type Context = Context; 48 | 49 | // notify `fanatic` of our address 50 | fn started(&mut self, ctx: &mut Self::Context) { 51 | self.fanatic_addr.do_send(SendExecutorAddr(ctx.address())); 52 | } 53 | } 54 | 55 | impl Executor

{ 56 | pub async fn new(config: ExecutorConfig

) -> eyre::Result> { 57 | let target_network = config.target.split('-').next().unwrap(); 58 | let uniswap_target = format!("{}-uniswap_v3", target_network); 59 | 60 | let aave_contracts = config 61 | .db_addr 62 | .send(database::GetProtocolContracts(config.target)) 63 | .await??; 64 | let uniswap_contracts = config 65 | .db_addr 66 | .send(database::GetProtocolContracts(uniswap_target)) 67 | .await??; 68 | 69 | let ( 70 | bot_contract, 71 | factory_contract, 72 | quoter_contract, 73 | pool_contract, 74 | datap_contract, 75 | provider_addr, 76 | ) = match ( 77 | uniswap_contracts.get("UniswapV3Factory"), 78 | uniswap_contracts.get("QuoterV2"), 79 | aave_contracts.get("Pool"), 80 | aave_contracts.get("UiPoolDataProviderV3"), 81 | aave_contracts.get("PoolAddressesProvider"), 82 | ) { 83 | ( 84 | Some(factory_addr), 85 | Some(quoter_addr), 86 | Some(pool_addr), 87 | Some(datap_addr), 88 | Some(provider_addr), 89 | ) => ( 90 | contracts::liquidator::LiquidatoorContract::new( 91 | config.bot_addr, 92 | config.provider.clone(), 93 | ), 94 | contracts::uniswap_v3::FactoryContract::new(*factory_addr, config.provider.clone()), 95 | contracts::uniswap_v3::QuoterContract::new(*quoter_addr, config.provider.clone()), 96 | contracts::aave_v3::PoolContract::new(*pool_addr, config.provider.clone()), 97 | contracts::aave_v3::DataProviderContract::new(*datap_addr, config.provider.clone()), 98 | *provider_addr, 99 | ), 100 | _ => return Err(eyre::eyre!("Missing required contract addresses")), 101 | }; 102 | 103 | Ok(Executor { 104 | provider: config.provider, 105 | 106 | db_addr: config.db_addr, 107 | fanatic_addr: config.fanatic_addr, 108 | provider_addr, 109 | 110 | bot_contract, 111 | pool_contract, 112 | datap_contract, 113 | factory_contract, 114 | quoter_contract, 115 | }) 116 | } 117 | } 118 | 119 | impl Handler for Executor

{ 120 | type Result = ResponseFuture>; 121 | 122 | fn handle(&mut self, msg: LiquidationRequest, _ctx: &mut Self::Context) -> Self::Result { 123 | let db_addr = self.db_addr.clone(); 124 | let fanatic_addr = self.fanatic_addr.clone(); 125 | let provider = self.provider.clone(); 126 | let provider_addr = self.provider_addr; 127 | let bot_contract = self.bot_contract.clone(); 128 | let pool_contract = self.pool_contract.clone(); 129 | let datap_contract = self.datap_contract.clone(); 130 | let factory_contract = self.factory_contract.clone(); 131 | 132 | let fut = async move { 133 | let health_factor = health_factor(&pool_contract, msg.user_address).await; 134 | 135 | if let Some(hf) = health_factor { 136 | info!(user = ?msg.user_address, health_factor = ?hf, "user is undercollateralized"); 137 | 138 | let indices = db_addr 139 | .send(database::GetReservesLiquidityIndices(format!( 140 | "{}-{}", 141 | msg.network, msg.protocol 142 | ))) 143 | .await??; 144 | 145 | let (debt_asset, collateral_asset, debt_to_cover) = user_liquidation_data( 146 | &datap_contract, 147 | provider_addr, 148 | msg.user_address, 149 | &indices, 150 | ) 151 | .await?; 152 | 153 | let (_pool_addr, fee) = find_most_liquid_uniswap_pool( 154 | &provider, 155 | &factory_contract, 156 | collateral_asset, 157 | debt_asset, 158 | ) 159 | .await?; 160 | 161 | match bot_contract 162 | .liquidatoor( 163 | debt_asset, 164 | collateral_asset, 165 | msg.user_address, 166 | debt_to_cover, 167 | Uint::from(fee), 168 | ) 169 | .call() 170 | .await 171 | { 172 | Ok(_) => { 173 | fanatic_addr 174 | .send(SuccessfulLiquidation { 175 | user_addr: msg.user_address, 176 | }) 177 | .await?; 178 | } 179 | Err(e) => { 180 | fanatic_addr 181 | .send(FailedLiquidation { 182 | user_addr: msg.user_address, 183 | }) 184 | .await?; 185 | error!("Liquidation failed: {}", e); 186 | } 187 | } 188 | } 189 | 190 | Ok(()) 191 | }; 192 | 193 | Box::pin(fut) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /abis/chainlink/CLRatePriceCapAdapter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "components": [ 6 | { 7 | "internalType": "contract IACLManager", 8 | "name": "aclManager", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "address", 13 | "name": "baseAggregatorAddress", 14 | "type": "address" 15 | }, 16 | { 17 | "internalType": "address", 18 | "name": "ratioProviderAddress", 19 | "type": "address" 20 | }, 21 | { 22 | "internalType": "string", 23 | "name": "pairDescription", 24 | "type": "string" 25 | }, 26 | { 27 | "internalType": "uint48", 28 | "name": "minimumSnapshotDelay", 29 | "type": "uint48" 30 | }, 31 | { 32 | "components": [ 33 | { 34 | "internalType": "uint104", 35 | "name": "snapshotRatio", 36 | "type": "uint104" 37 | }, 38 | { 39 | "internalType": "uint48", 40 | "name": "snapshotTimestamp", 41 | "type": "uint48" 42 | }, 43 | { 44 | "internalType": "uint16", 45 | "name": "maxYearlyRatioGrowthPercent", 46 | "type": "uint16" 47 | } 48 | ], 49 | "internalType": "struct IPriceCapAdapter.PriceCapUpdateParams", 50 | "name": "priceCapParams", 51 | "type": "tuple" 52 | } 53 | ], 54 | "internalType": "struct IPriceCapAdapter.CapAdapterParams", 55 | "name": "capAdapterParams", 56 | "type": "tuple" 57 | } 58 | ], 59 | "stateMutability": "nonpayable", 60 | "type": "constructor" 61 | }, 62 | { 63 | "inputs": [], 64 | "name": "ACLManagerIsZeroAddress", 65 | "type": "error" 66 | }, 67 | { 68 | "inputs": [], 69 | "name": "CallerIsNotRiskOrPoolAdmin", 70 | "type": "error" 71 | }, 72 | { 73 | "inputs": [], 74 | "name": "DecimalsAboveLimit", 75 | "type": "error" 76 | }, 77 | { 78 | "inputs": [], 79 | "name": "DecimalsNotEqual", 80 | "type": "error" 81 | }, 82 | { 83 | "inputs": [ 84 | { 85 | "internalType": "uint48", 86 | "name": "timestamp", 87 | "type": "uint48" 88 | } 89 | ], 90 | "name": "InvalidRatioTimestamp", 91 | "type": "error" 92 | }, 93 | { 94 | "inputs": [], 95 | "name": "RatioOutOfBounds", 96 | "type": "error" 97 | }, 98 | { 99 | "inputs": [ 100 | { 101 | "internalType": "uint104", 102 | "name": "snapshotRatio", 103 | "type": "uint104" 104 | }, 105 | { 106 | "internalType": "uint16", 107 | "name": "maxYearlyRatioGrowthPercent", 108 | "type": "uint16" 109 | } 110 | ], 111 | "name": "SnapshotMayOverflowSoon", 112 | "type": "error" 113 | }, 114 | { 115 | "inputs": [], 116 | "name": "SnapshotRatioIsZero", 117 | "type": "error" 118 | }, 119 | { 120 | "anonymous": false, 121 | "inputs": [ 122 | { 123 | "indexed": false, 124 | "internalType": "uint256", 125 | "name": "snapshotRatio", 126 | "type": "uint256" 127 | }, 128 | { 129 | "indexed": false, 130 | "internalType": "uint256", 131 | "name": "snapshotTimestamp", 132 | "type": "uint256" 133 | }, 134 | { 135 | "indexed": false, 136 | "internalType": "uint256", 137 | "name": "maxRatioGrowthPerSecond", 138 | "type": "uint256" 139 | }, 140 | { 141 | "indexed": false, 142 | "internalType": "uint16", 143 | "name": "maxYearlyRatioGrowthPercent", 144 | "type": "uint16" 145 | } 146 | ], 147 | "name": "CapParametersUpdated", 148 | "type": "event" 149 | }, 150 | { 151 | "inputs": [], 152 | "name": "ACL_MANAGER", 153 | "outputs": [ 154 | { 155 | "internalType": "contract IACLManager", 156 | "name": "", 157 | "type": "address" 158 | } 159 | ], 160 | "stateMutability": "view", 161 | "type": "function" 162 | }, 163 | { 164 | "inputs": [], 165 | "name": "BASE_TO_USD_AGGREGATOR", 166 | "outputs": [ 167 | { 168 | "internalType": "contract IChainlinkAggregator", 169 | "name": "", 170 | "type": "address" 171 | } 172 | ], 173 | "stateMutability": "view", 174 | "type": "function" 175 | }, 176 | { 177 | "inputs": [], 178 | "name": "DECIMALS", 179 | "outputs": [ 180 | { 181 | "internalType": "uint8", 182 | "name": "", 183 | "type": "uint8" 184 | } 185 | ], 186 | "stateMutability": "view", 187 | "type": "function" 188 | }, 189 | { 190 | "inputs": [], 191 | "name": "MINIMAL_RATIO_INCREASE_LIFETIME", 192 | "outputs": [ 193 | { 194 | "internalType": "uint256", 195 | "name": "", 196 | "type": "uint256" 197 | } 198 | ], 199 | "stateMutability": "view", 200 | "type": "function" 201 | }, 202 | { 203 | "inputs": [], 204 | "name": "MINIMUM_SNAPSHOT_DELAY", 205 | "outputs": [ 206 | { 207 | "internalType": "uint48", 208 | "name": "", 209 | "type": "uint48" 210 | } 211 | ], 212 | "stateMutability": "view", 213 | "type": "function" 214 | }, 215 | { 216 | "inputs": [], 217 | "name": "PERCENTAGE_FACTOR", 218 | "outputs": [ 219 | { 220 | "internalType": "uint256", 221 | "name": "", 222 | "type": "uint256" 223 | } 224 | ], 225 | "stateMutability": "view", 226 | "type": "function" 227 | }, 228 | { 229 | "inputs": [], 230 | "name": "RATIO_DECIMALS", 231 | "outputs": [ 232 | { 233 | "internalType": "uint8", 234 | "name": "", 235 | "type": "uint8" 236 | } 237 | ], 238 | "stateMutability": "view", 239 | "type": "function" 240 | }, 241 | { 242 | "inputs": [], 243 | "name": "RATIO_PROVIDER", 244 | "outputs": [ 245 | { 246 | "internalType": "address", 247 | "name": "", 248 | "type": "address" 249 | } 250 | ], 251 | "stateMutability": "view", 252 | "type": "function" 253 | }, 254 | { 255 | "inputs": [], 256 | "name": "SECONDS_PER_YEAR", 257 | "outputs": [ 258 | { 259 | "internalType": "uint256", 260 | "name": "", 261 | "type": "uint256" 262 | } 263 | ], 264 | "stateMutability": "view", 265 | "type": "function" 266 | }, 267 | { 268 | "inputs": [], 269 | "name": "decimals", 270 | "outputs": [ 271 | { 272 | "internalType": "uint8", 273 | "name": "", 274 | "type": "uint8" 275 | } 276 | ], 277 | "stateMutability": "view", 278 | "type": "function" 279 | }, 280 | { 281 | "inputs": [], 282 | "name": "description", 283 | "outputs": [ 284 | { 285 | "internalType": "string", 286 | "name": "", 287 | "type": "string" 288 | } 289 | ], 290 | "stateMutability": "view", 291 | "type": "function" 292 | }, 293 | { 294 | "inputs": [], 295 | "name": "getMaxRatioGrowthPerSecond", 296 | "outputs": [ 297 | { 298 | "internalType": "uint256", 299 | "name": "", 300 | "type": "uint256" 301 | } 302 | ], 303 | "stateMutability": "view", 304 | "type": "function" 305 | }, 306 | { 307 | "inputs": [], 308 | "name": "getMaxYearlyGrowthRatePercent", 309 | "outputs": [ 310 | { 311 | "internalType": "uint256", 312 | "name": "", 313 | "type": "uint256" 314 | } 315 | ], 316 | "stateMutability": "view", 317 | "type": "function" 318 | }, 319 | { 320 | "inputs": [], 321 | "name": "getRatio", 322 | "outputs": [ 323 | { 324 | "internalType": "int256", 325 | "name": "", 326 | "type": "int256" 327 | } 328 | ], 329 | "stateMutability": "view", 330 | "type": "function" 331 | }, 332 | { 333 | "inputs": [], 334 | "name": "getSnapshotRatio", 335 | "outputs": [ 336 | { 337 | "internalType": "uint256", 338 | "name": "", 339 | "type": "uint256" 340 | } 341 | ], 342 | "stateMutability": "view", 343 | "type": "function" 344 | }, 345 | { 346 | "inputs": [], 347 | "name": "getSnapshotTimestamp", 348 | "outputs": [ 349 | { 350 | "internalType": "uint256", 351 | "name": "", 352 | "type": "uint256" 353 | } 354 | ], 355 | "stateMutability": "view", 356 | "type": "function" 357 | }, 358 | { 359 | "inputs": [], 360 | "name": "isCapped", 361 | "outputs": [ 362 | { 363 | "internalType": "bool", 364 | "name": "", 365 | "type": "bool" 366 | } 367 | ], 368 | "stateMutability": "view", 369 | "type": "function" 370 | }, 371 | { 372 | "inputs": [], 373 | "name": "latestAnswer", 374 | "outputs": [ 375 | { 376 | "internalType": "int256", 377 | "name": "", 378 | "type": "int256" 379 | } 380 | ], 381 | "stateMutability": "view", 382 | "type": "function" 383 | }, 384 | { 385 | "inputs": [ 386 | { 387 | "components": [ 388 | { 389 | "internalType": "uint104", 390 | "name": "snapshotRatio", 391 | "type": "uint104" 392 | }, 393 | { 394 | "internalType": "uint48", 395 | "name": "snapshotTimestamp", 396 | "type": "uint48" 397 | }, 398 | { 399 | "internalType": "uint16", 400 | "name": "maxYearlyRatioGrowthPercent", 401 | "type": "uint16" 402 | } 403 | ], 404 | "internalType": "struct IPriceCapAdapter.PriceCapUpdateParams", 405 | "name": "priceCapParams", 406 | "type": "tuple" 407 | } 408 | ], 409 | "name": "setCapParameters", 410 | "outputs": [], 411 | "stateMutability": "nonpayable", 412 | "type": "function" 413 | } 414 | ] -------------------------------------------------------------------------------- /abis/chainlink/EACAggregatorProxy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_aggregator", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_accessController", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [ 21 | { 22 | "indexed": true, 23 | "internalType": "int256", 24 | "name": "current", 25 | "type": "int256" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "uint256", 30 | "name": "roundId", 31 | "type": "uint256" 32 | }, 33 | { 34 | "indexed": false, 35 | "internalType": "uint256", 36 | "name": "updatedAt", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "AnswerUpdated", 41 | "type": "event" 42 | }, 43 | { 44 | "anonymous": false, 45 | "inputs": [ 46 | { 47 | "indexed": true, 48 | "internalType": "uint256", 49 | "name": "roundId", 50 | "type": "uint256" 51 | }, 52 | { 53 | "indexed": true, 54 | "internalType": "address", 55 | "name": "startedBy", 56 | "type": "address" 57 | }, 58 | { 59 | "indexed": false, 60 | "internalType": "uint256", 61 | "name": "startedAt", 62 | "type": "uint256" 63 | } 64 | ], 65 | "name": "NewRound", 66 | "type": "event" 67 | }, 68 | { 69 | "anonymous": false, 70 | "inputs": [ 71 | { 72 | "indexed": true, 73 | "internalType": "address", 74 | "name": "from", 75 | "type": "address" 76 | }, 77 | { 78 | "indexed": true, 79 | "internalType": "address", 80 | "name": "to", 81 | "type": "address" 82 | } 83 | ], 84 | "name": "OwnershipTransferRequested", 85 | "type": "event" 86 | }, 87 | { 88 | "anonymous": false, 89 | "inputs": [ 90 | { 91 | "indexed": true, 92 | "internalType": "address", 93 | "name": "from", 94 | "type": "address" 95 | }, 96 | { 97 | "indexed": true, 98 | "internalType": "address", 99 | "name": "to", 100 | "type": "address" 101 | } 102 | ], 103 | "name": "OwnershipTransferred", 104 | "type": "event" 105 | }, 106 | { 107 | "inputs": [], 108 | "name": "acceptOwnership", 109 | "outputs": [], 110 | "stateMutability": "nonpayable", 111 | "type": "function" 112 | }, 113 | { 114 | "inputs": [], 115 | "name": "accessController", 116 | "outputs": [ 117 | { 118 | "internalType": "contract AccessControllerInterface", 119 | "name": "", 120 | "type": "address" 121 | } 122 | ], 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [], 128 | "name": "aggregator", 129 | "outputs": [ 130 | { 131 | "internalType": "address", 132 | "name": "", 133 | "type": "address" 134 | } 135 | ], 136 | "stateMutability": "view", 137 | "type": "function" 138 | }, 139 | { 140 | "inputs": [ 141 | { 142 | "internalType": "address", 143 | "name": "_aggregator", 144 | "type": "address" 145 | } 146 | ], 147 | "name": "confirmAggregator", 148 | "outputs": [], 149 | "stateMutability": "nonpayable", 150 | "type": "function" 151 | }, 152 | { 153 | "inputs": [], 154 | "name": "decimals", 155 | "outputs": [ 156 | { 157 | "internalType": "uint8", 158 | "name": "", 159 | "type": "uint8" 160 | } 161 | ], 162 | "stateMutability": "view", 163 | "type": "function" 164 | }, 165 | { 166 | "inputs": [], 167 | "name": "description", 168 | "outputs": [ 169 | { 170 | "internalType": "string", 171 | "name": "", 172 | "type": "string" 173 | } 174 | ], 175 | "stateMutability": "view", 176 | "type": "function" 177 | }, 178 | { 179 | "inputs": [ 180 | { 181 | "internalType": "uint256", 182 | "name": "_roundId", 183 | "type": "uint256" 184 | } 185 | ], 186 | "name": "getAnswer", 187 | "outputs": [ 188 | { 189 | "internalType": "int256", 190 | "name": "", 191 | "type": "int256" 192 | } 193 | ], 194 | "stateMutability": "view", 195 | "type": "function" 196 | }, 197 | { 198 | "inputs": [ 199 | { 200 | "internalType": "uint80", 201 | "name": "_roundId", 202 | "type": "uint80" 203 | } 204 | ], 205 | "name": "getRoundData", 206 | "outputs": [ 207 | { 208 | "internalType": "uint80", 209 | "name": "roundId", 210 | "type": "uint80" 211 | }, 212 | { 213 | "internalType": "int256", 214 | "name": "answer", 215 | "type": "int256" 216 | }, 217 | { 218 | "internalType": "uint256", 219 | "name": "startedAt", 220 | "type": "uint256" 221 | }, 222 | { 223 | "internalType": "uint256", 224 | "name": "updatedAt", 225 | "type": "uint256" 226 | }, 227 | { 228 | "internalType": "uint80", 229 | "name": "answeredInRound", 230 | "type": "uint80" 231 | } 232 | ], 233 | "stateMutability": "view", 234 | "type": "function" 235 | }, 236 | { 237 | "inputs": [ 238 | { 239 | "internalType": "uint256", 240 | "name": "_roundId", 241 | "type": "uint256" 242 | } 243 | ], 244 | "name": "getTimestamp", 245 | "outputs": [ 246 | { 247 | "internalType": "uint256", 248 | "name": "", 249 | "type": "uint256" 250 | } 251 | ], 252 | "stateMutability": "view", 253 | "type": "function" 254 | }, 255 | { 256 | "inputs": [], 257 | "name": "latestAnswer", 258 | "outputs": [ 259 | { 260 | "internalType": "int256", 261 | "name": "", 262 | "type": "int256" 263 | } 264 | ], 265 | "stateMutability": "view", 266 | "type": "function" 267 | }, 268 | { 269 | "inputs": [], 270 | "name": "latestRound", 271 | "outputs": [ 272 | { 273 | "internalType": "uint256", 274 | "name": "", 275 | "type": "uint256" 276 | } 277 | ], 278 | "stateMutability": "view", 279 | "type": "function" 280 | }, 281 | { 282 | "inputs": [], 283 | "name": "latestRoundData", 284 | "outputs": [ 285 | { 286 | "internalType": "uint80", 287 | "name": "roundId", 288 | "type": "uint80" 289 | }, 290 | { 291 | "internalType": "int256", 292 | "name": "answer", 293 | "type": "int256" 294 | }, 295 | { 296 | "internalType": "uint256", 297 | "name": "startedAt", 298 | "type": "uint256" 299 | }, 300 | { 301 | "internalType": "uint256", 302 | "name": "updatedAt", 303 | "type": "uint256" 304 | }, 305 | { 306 | "internalType": "uint80", 307 | "name": "answeredInRound", 308 | "type": "uint80" 309 | } 310 | ], 311 | "stateMutability": "view", 312 | "type": "function" 313 | }, 314 | { 315 | "inputs": [], 316 | "name": "latestTimestamp", 317 | "outputs": [ 318 | { 319 | "internalType": "uint256", 320 | "name": "", 321 | "type": "uint256" 322 | } 323 | ], 324 | "stateMutability": "view", 325 | "type": "function" 326 | }, 327 | { 328 | "inputs": [], 329 | "name": "owner", 330 | "outputs": [ 331 | { 332 | "internalType": "address", 333 | "name": "", 334 | "type": "address" 335 | } 336 | ], 337 | "stateMutability": "view", 338 | "type": "function" 339 | }, 340 | { 341 | "inputs": [ 342 | { 343 | "internalType": "uint16", 344 | "name": "", 345 | "type": "uint16" 346 | } 347 | ], 348 | "name": "phaseAggregators", 349 | "outputs": [ 350 | { 351 | "internalType": "contract AggregatorV2V3Interface", 352 | "name": "", 353 | "type": "address" 354 | } 355 | ], 356 | "stateMutability": "view", 357 | "type": "function" 358 | }, 359 | { 360 | "inputs": [], 361 | "name": "phaseId", 362 | "outputs": [ 363 | { 364 | "internalType": "uint16", 365 | "name": "", 366 | "type": "uint16" 367 | } 368 | ], 369 | "stateMutability": "view", 370 | "type": "function" 371 | }, 372 | { 373 | "inputs": [ 374 | { 375 | "internalType": "address", 376 | "name": "_aggregator", 377 | "type": "address" 378 | } 379 | ], 380 | "name": "proposeAggregator", 381 | "outputs": [], 382 | "stateMutability": "nonpayable", 383 | "type": "function" 384 | }, 385 | { 386 | "inputs": [], 387 | "name": "proposedAggregator", 388 | "outputs": [ 389 | { 390 | "internalType": "contract AggregatorV2V3Interface", 391 | "name": "", 392 | "type": "address" 393 | } 394 | ], 395 | "stateMutability": "view", 396 | "type": "function" 397 | }, 398 | { 399 | "inputs": [ 400 | { 401 | "internalType": "uint80", 402 | "name": "_roundId", 403 | "type": "uint80" 404 | } 405 | ], 406 | "name": "proposedGetRoundData", 407 | "outputs": [ 408 | { 409 | "internalType": "uint80", 410 | "name": "roundId", 411 | "type": "uint80" 412 | }, 413 | { 414 | "internalType": "int256", 415 | "name": "answer", 416 | "type": "int256" 417 | }, 418 | { 419 | "internalType": "uint256", 420 | "name": "startedAt", 421 | "type": "uint256" 422 | }, 423 | { 424 | "internalType": "uint256", 425 | "name": "updatedAt", 426 | "type": "uint256" 427 | }, 428 | { 429 | "internalType": "uint80", 430 | "name": "answeredInRound", 431 | "type": "uint80" 432 | } 433 | ], 434 | "stateMutability": "view", 435 | "type": "function" 436 | }, 437 | { 438 | "inputs": [], 439 | "name": "proposedLatestRoundData", 440 | "outputs": [ 441 | { 442 | "internalType": "uint80", 443 | "name": "roundId", 444 | "type": "uint80" 445 | }, 446 | { 447 | "internalType": "int256", 448 | "name": "answer", 449 | "type": "int256" 450 | }, 451 | { 452 | "internalType": "uint256", 453 | "name": "startedAt", 454 | "type": "uint256" 455 | }, 456 | { 457 | "internalType": "uint256", 458 | "name": "updatedAt", 459 | "type": "uint256" 460 | }, 461 | { 462 | "internalType": "uint80", 463 | "name": "answeredInRound", 464 | "type": "uint80" 465 | } 466 | ], 467 | "stateMutability": "view", 468 | "type": "function" 469 | }, 470 | { 471 | "inputs": [ 472 | { 473 | "internalType": "address", 474 | "name": "_accessController", 475 | "type": "address" 476 | } 477 | ], 478 | "name": "setController", 479 | "outputs": [], 480 | "stateMutability": "nonpayable", 481 | "type": "function" 482 | }, 483 | { 484 | "inputs": [ 485 | { 486 | "internalType": "address", 487 | "name": "_to", 488 | "type": "address" 489 | } 490 | ], 491 | "name": "transferOwnership", 492 | "outputs": [], 493 | "stateMutability": "nonpayable", 494 | "type": "function" 495 | }, 496 | { 497 | "inputs": [], 498 | "name": "version", 499 | "outputs": [ 500 | { 501 | "internalType": "uint256", 502 | "name": "", 503 | "type": "uint256" 504 | } 505 | ], 506 | "stateMutability": "view", 507 | "type": "function" 508 | } 509 | ] -------------------------------------------------------------------------------- /abis/aave_v3/PoolAddressesProvider.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "marketId", 7 | "type": "string" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "owner", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [ 21 | { 22 | "indexed": true, 23 | "internalType": "address", 24 | "name": "oldAddress", 25 | "type": "address" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "address", 30 | "name": "newAddress", 31 | "type": "address" 32 | } 33 | ], 34 | "name": "ACLAdminUpdated", 35 | "type": "event" 36 | }, 37 | { 38 | "anonymous": false, 39 | "inputs": [ 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "oldAddress", 44 | "type": "address" 45 | }, 46 | { 47 | "indexed": true, 48 | "internalType": "address", 49 | "name": "newAddress", 50 | "type": "address" 51 | } 52 | ], 53 | "name": "ACLManagerUpdated", 54 | "type": "event" 55 | }, 56 | { 57 | "anonymous": false, 58 | "inputs": [ 59 | { 60 | "indexed": true, 61 | "internalType": "bytes32", 62 | "name": "id", 63 | "type": "bytes32" 64 | }, 65 | { 66 | "indexed": true, 67 | "internalType": "address", 68 | "name": "oldAddress", 69 | "type": "address" 70 | }, 71 | { 72 | "indexed": true, 73 | "internalType": "address", 74 | "name": "newAddress", 75 | "type": "address" 76 | } 77 | ], 78 | "name": "AddressSet", 79 | "type": "event" 80 | }, 81 | { 82 | "anonymous": false, 83 | "inputs": [ 84 | { 85 | "indexed": true, 86 | "internalType": "bytes32", 87 | "name": "id", 88 | "type": "bytes32" 89 | }, 90 | { 91 | "indexed": true, 92 | "internalType": "address", 93 | "name": "proxyAddress", 94 | "type": "address" 95 | }, 96 | { 97 | "indexed": false, 98 | "internalType": "address", 99 | "name": "oldImplementationAddress", 100 | "type": "address" 101 | }, 102 | { 103 | "indexed": true, 104 | "internalType": "address", 105 | "name": "newImplementationAddress", 106 | "type": "address" 107 | } 108 | ], 109 | "name": "AddressSetAsProxy", 110 | "type": "event" 111 | }, 112 | { 113 | "anonymous": false, 114 | "inputs": [ 115 | { 116 | "indexed": true, 117 | "internalType": "string", 118 | "name": "oldMarketId", 119 | "type": "string" 120 | }, 121 | { 122 | "indexed": true, 123 | "internalType": "string", 124 | "name": "newMarketId", 125 | "type": "string" 126 | } 127 | ], 128 | "name": "MarketIdSet", 129 | "type": "event" 130 | }, 131 | { 132 | "anonymous": false, 133 | "inputs": [ 134 | { 135 | "indexed": true, 136 | "internalType": "address", 137 | "name": "previousOwner", 138 | "type": "address" 139 | }, 140 | { 141 | "indexed": true, 142 | "internalType": "address", 143 | "name": "newOwner", 144 | "type": "address" 145 | } 146 | ], 147 | "name": "OwnershipTransferred", 148 | "type": "event" 149 | }, 150 | { 151 | "anonymous": false, 152 | "inputs": [ 153 | { 154 | "indexed": true, 155 | "internalType": "address", 156 | "name": "oldAddress", 157 | "type": "address" 158 | }, 159 | { 160 | "indexed": true, 161 | "internalType": "address", 162 | "name": "newAddress", 163 | "type": "address" 164 | } 165 | ], 166 | "name": "PoolConfiguratorUpdated", 167 | "type": "event" 168 | }, 169 | { 170 | "anonymous": false, 171 | "inputs": [ 172 | { 173 | "indexed": true, 174 | "internalType": "address", 175 | "name": "oldAddress", 176 | "type": "address" 177 | }, 178 | { 179 | "indexed": true, 180 | "internalType": "address", 181 | "name": "newAddress", 182 | "type": "address" 183 | } 184 | ], 185 | "name": "PoolDataProviderUpdated", 186 | "type": "event" 187 | }, 188 | { 189 | "anonymous": false, 190 | "inputs": [ 191 | { 192 | "indexed": true, 193 | "internalType": "address", 194 | "name": "oldAddress", 195 | "type": "address" 196 | }, 197 | { 198 | "indexed": true, 199 | "internalType": "address", 200 | "name": "newAddress", 201 | "type": "address" 202 | } 203 | ], 204 | "name": "PoolUpdated", 205 | "type": "event" 206 | }, 207 | { 208 | "anonymous": false, 209 | "inputs": [ 210 | { 211 | "indexed": true, 212 | "internalType": "address", 213 | "name": "oldAddress", 214 | "type": "address" 215 | }, 216 | { 217 | "indexed": true, 218 | "internalType": "address", 219 | "name": "newAddress", 220 | "type": "address" 221 | } 222 | ], 223 | "name": "PriceOracleSentinelUpdated", 224 | "type": "event" 225 | }, 226 | { 227 | "anonymous": false, 228 | "inputs": [ 229 | { 230 | "indexed": true, 231 | "internalType": "address", 232 | "name": "oldAddress", 233 | "type": "address" 234 | }, 235 | { 236 | "indexed": true, 237 | "internalType": "address", 238 | "name": "newAddress", 239 | "type": "address" 240 | } 241 | ], 242 | "name": "PriceOracleUpdated", 243 | "type": "event" 244 | }, 245 | { 246 | "anonymous": false, 247 | "inputs": [ 248 | { 249 | "indexed": true, 250 | "internalType": "bytes32", 251 | "name": "id", 252 | "type": "bytes32" 253 | }, 254 | { 255 | "indexed": true, 256 | "internalType": "address", 257 | "name": "proxyAddress", 258 | "type": "address" 259 | }, 260 | { 261 | "indexed": true, 262 | "internalType": "address", 263 | "name": "implementationAddress", 264 | "type": "address" 265 | } 266 | ], 267 | "name": "ProxyCreated", 268 | "type": "event" 269 | }, 270 | { 271 | "inputs": [], 272 | "name": "getACLAdmin", 273 | "outputs": [ 274 | { 275 | "internalType": "address", 276 | "name": "", 277 | "type": "address" 278 | } 279 | ], 280 | "stateMutability": "view", 281 | "type": "function" 282 | }, 283 | { 284 | "inputs": [], 285 | "name": "getACLManager", 286 | "outputs": [ 287 | { 288 | "internalType": "address", 289 | "name": "", 290 | "type": "address" 291 | } 292 | ], 293 | "stateMutability": "view", 294 | "type": "function" 295 | }, 296 | { 297 | "inputs": [ 298 | { 299 | "internalType": "bytes32", 300 | "name": "id", 301 | "type": "bytes32" 302 | } 303 | ], 304 | "name": "getAddress", 305 | "outputs": [ 306 | { 307 | "internalType": "address", 308 | "name": "", 309 | "type": "address" 310 | } 311 | ], 312 | "stateMutability": "view", 313 | "type": "function" 314 | }, 315 | { 316 | "inputs": [], 317 | "name": "getMarketId", 318 | "outputs": [ 319 | { 320 | "internalType": "string", 321 | "name": "", 322 | "type": "string" 323 | } 324 | ], 325 | "stateMutability": "view", 326 | "type": "function" 327 | }, 328 | { 329 | "inputs": [], 330 | "name": "getPool", 331 | "outputs": [ 332 | { 333 | "internalType": "address", 334 | "name": "", 335 | "type": "address" 336 | } 337 | ], 338 | "stateMutability": "view", 339 | "type": "function" 340 | }, 341 | { 342 | "inputs": [], 343 | "name": "getPoolConfigurator", 344 | "outputs": [ 345 | { 346 | "internalType": "address", 347 | "name": "", 348 | "type": "address" 349 | } 350 | ], 351 | "stateMutability": "view", 352 | "type": "function" 353 | }, 354 | { 355 | "inputs": [], 356 | "name": "getPoolDataProvider", 357 | "outputs": [ 358 | { 359 | "internalType": "address", 360 | "name": "", 361 | "type": "address" 362 | } 363 | ], 364 | "stateMutability": "view", 365 | "type": "function" 366 | }, 367 | { 368 | "inputs": [], 369 | "name": "getPriceOracle", 370 | "outputs": [ 371 | { 372 | "internalType": "address", 373 | "name": "", 374 | "type": "address" 375 | } 376 | ], 377 | "stateMutability": "view", 378 | "type": "function" 379 | }, 380 | { 381 | "inputs": [], 382 | "name": "getPriceOracleSentinel", 383 | "outputs": [ 384 | { 385 | "internalType": "address", 386 | "name": "", 387 | "type": "address" 388 | } 389 | ], 390 | "stateMutability": "view", 391 | "type": "function" 392 | }, 393 | { 394 | "inputs": [], 395 | "name": "owner", 396 | "outputs": [ 397 | { 398 | "internalType": "address", 399 | "name": "", 400 | "type": "address" 401 | } 402 | ], 403 | "stateMutability": "view", 404 | "type": "function" 405 | }, 406 | { 407 | "inputs": [], 408 | "name": "renounceOwnership", 409 | "outputs": [], 410 | "stateMutability": "nonpayable", 411 | "type": "function" 412 | }, 413 | { 414 | "inputs": [ 415 | { 416 | "internalType": "address", 417 | "name": "newAclAdmin", 418 | "type": "address" 419 | } 420 | ], 421 | "name": "setACLAdmin", 422 | "outputs": [], 423 | "stateMutability": "nonpayable", 424 | "type": "function" 425 | }, 426 | { 427 | "inputs": [ 428 | { 429 | "internalType": "address", 430 | "name": "newAclManager", 431 | "type": "address" 432 | } 433 | ], 434 | "name": "setACLManager", 435 | "outputs": [], 436 | "stateMutability": "nonpayable", 437 | "type": "function" 438 | }, 439 | { 440 | "inputs": [ 441 | { 442 | "internalType": "bytes32", 443 | "name": "id", 444 | "type": "bytes32" 445 | }, 446 | { 447 | "internalType": "address", 448 | "name": "newAddress", 449 | "type": "address" 450 | } 451 | ], 452 | "name": "setAddress", 453 | "outputs": [], 454 | "stateMutability": "nonpayable", 455 | "type": "function" 456 | }, 457 | { 458 | "inputs": [ 459 | { 460 | "internalType": "bytes32", 461 | "name": "id", 462 | "type": "bytes32" 463 | }, 464 | { 465 | "internalType": "address", 466 | "name": "newImplementationAddress", 467 | "type": "address" 468 | } 469 | ], 470 | "name": "setAddressAsProxy", 471 | "outputs": [], 472 | "stateMutability": "nonpayable", 473 | "type": "function" 474 | }, 475 | { 476 | "inputs": [ 477 | { 478 | "internalType": "string", 479 | "name": "newMarketId", 480 | "type": "string" 481 | } 482 | ], 483 | "name": "setMarketId", 484 | "outputs": [], 485 | "stateMutability": "nonpayable", 486 | "type": "function" 487 | }, 488 | { 489 | "inputs": [ 490 | { 491 | "internalType": "address", 492 | "name": "newPoolConfiguratorImpl", 493 | "type": "address" 494 | } 495 | ], 496 | "name": "setPoolConfiguratorImpl", 497 | "outputs": [], 498 | "stateMutability": "nonpayable", 499 | "type": "function" 500 | }, 501 | { 502 | "inputs": [ 503 | { 504 | "internalType": "address", 505 | "name": "newDataProvider", 506 | "type": "address" 507 | } 508 | ], 509 | "name": "setPoolDataProvider", 510 | "outputs": [], 511 | "stateMutability": "nonpayable", 512 | "type": "function" 513 | }, 514 | { 515 | "inputs": [ 516 | { 517 | "internalType": "address", 518 | "name": "newPoolImpl", 519 | "type": "address" 520 | } 521 | ], 522 | "name": "setPoolImpl", 523 | "outputs": [], 524 | "stateMutability": "nonpayable", 525 | "type": "function" 526 | }, 527 | { 528 | "inputs": [ 529 | { 530 | "internalType": "address", 531 | "name": "newPriceOracle", 532 | "type": "address" 533 | } 534 | ], 535 | "name": "setPriceOracle", 536 | "outputs": [], 537 | "stateMutability": "nonpayable", 538 | "type": "function" 539 | }, 540 | { 541 | "inputs": [ 542 | { 543 | "internalType": "address", 544 | "name": "newPriceOracleSentinel", 545 | "type": "address" 546 | } 547 | ], 548 | "name": "setPriceOracleSentinel", 549 | "outputs": [], 550 | "stateMutability": "nonpayable", 551 | "type": "function" 552 | }, 553 | { 554 | "inputs": [ 555 | { 556 | "internalType": "address", 557 | "name": "newOwner", 558 | "type": "address" 559 | } 560 | ], 561 | "name": "transferOwnership", 562 | "outputs": [], 563 | "stateMutability": "nonpayable", 564 | "type": "function" 565 | } 566 | ] -------------------------------------------------------------------------------- /abis/aave_v3/AaveProtocolDataProvider.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "contract IPoolAddressesProvider", 6 | "name": "addressesProvider", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "inputs": [], 15 | "name": "ADDRESSES_PROVIDER", 16 | "outputs": [ 17 | { 18 | "internalType": "contract IPoolAddressesProvider", 19 | "name": "", 20 | "type": "address" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | }, 26 | { 27 | "inputs": [ 28 | { 29 | "internalType": "address", 30 | "name": "asset", 31 | "type": "address" 32 | } 33 | ], 34 | "name": "getATokenTotalSupply", 35 | "outputs": [ 36 | { 37 | "internalType": "uint256", 38 | "name": "", 39 | "type": "uint256" 40 | } 41 | ], 42 | "stateMutability": "view", 43 | "type": "function" 44 | }, 45 | { 46 | "inputs": [], 47 | "name": "getAllATokens", 48 | "outputs": [ 49 | { 50 | "components": [ 51 | { 52 | "internalType": "string", 53 | "name": "symbol", 54 | "type": "string" 55 | }, 56 | { 57 | "internalType": "address", 58 | "name": "tokenAddress", 59 | "type": "address" 60 | } 61 | ], 62 | "internalType": "struct IPoolDataProvider.TokenData[]", 63 | "name": "", 64 | "type": "tuple[]" 65 | } 66 | ], 67 | "stateMutability": "view", 68 | "type": "function" 69 | }, 70 | { 71 | "inputs": [], 72 | "name": "getAllReservesTokens", 73 | "outputs": [ 74 | { 75 | "components": [ 76 | { 77 | "internalType": "string", 78 | "name": "symbol", 79 | "type": "string" 80 | }, 81 | { 82 | "internalType": "address", 83 | "name": "tokenAddress", 84 | "type": "address" 85 | } 86 | ], 87 | "internalType": "struct IPoolDataProvider.TokenData[]", 88 | "name": "", 89 | "type": "tuple[]" 90 | } 91 | ], 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "inputs": [ 97 | { 98 | "internalType": "address", 99 | "name": "asset", 100 | "type": "address" 101 | } 102 | ], 103 | "name": "getDebtCeiling", 104 | "outputs": [ 105 | { 106 | "internalType": "uint256", 107 | "name": "", 108 | "type": "uint256" 109 | } 110 | ], 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "inputs": [], 116 | "name": "getDebtCeilingDecimals", 117 | "outputs": [ 118 | { 119 | "internalType": "uint256", 120 | "name": "", 121 | "type": "uint256" 122 | } 123 | ], 124 | "stateMutability": "pure", 125 | "type": "function" 126 | }, 127 | { 128 | "inputs": [ 129 | { 130 | "internalType": "address", 131 | "name": "asset", 132 | "type": "address" 133 | } 134 | ], 135 | "name": "getFlashLoanEnabled", 136 | "outputs": [ 137 | { 138 | "internalType": "bool", 139 | "name": "", 140 | "type": "bool" 141 | } 142 | ], 143 | "stateMutability": "view", 144 | "type": "function" 145 | }, 146 | { 147 | "inputs": [ 148 | { 149 | "internalType": "address", 150 | "name": "asset", 151 | "type": "address" 152 | } 153 | ], 154 | "name": "getInterestRateStrategyAddress", 155 | "outputs": [ 156 | { 157 | "internalType": "address", 158 | "name": "irStrategyAddress", 159 | "type": "address" 160 | } 161 | ], 162 | "stateMutability": "view", 163 | "type": "function" 164 | }, 165 | { 166 | "inputs": [ 167 | { 168 | "internalType": "address", 169 | "name": "asset", 170 | "type": "address" 171 | } 172 | ], 173 | "name": "getIsVirtualAccActive", 174 | "outputs": [ 175 | { 176 | "internalType": "bool", 177 | "name": "", 178 | "type": "bool" 179 | } 180 | ], 181 | "stateMutability": "view", 182 | "type": "function" 183 | }, 184 | { 185 | "inputs": [ 186 | { 187 | "internalType": "address", 188 | "name": "asset", 189 | "type": "address" 190 | } 191 | ], 192 | "name": "getLiquidationProtocolFee", 193 | "outputs": [ 194 | { 195 | "internalType": "uint256", 196 | "name": "", 197 | "type": "uint256" 198 | } 199 | ], 200 | "stateMutability": "view", 201 | "type": "function" 202 | }, 203 | { 204 | "inputs": [ 205 | { 206 | "internalType": "address", 207 | "name": "asset", 208 | "type": "address" 209 | } 210 | ], 211 | "name": "getPaused", 212 | "outputs": [ 213 | { 214 | "internalType": "bool", 215 | "name": "isPaused", 216 | "type": "bool" 217 | } 218 | ], 219 | "stateMutability": "view", 220 | "type": "function" 221 | }, 222 | { 223 | "inputs": [ 224 | { 225 | "internalType": "address", 226 | "name": "asset", 227 | "type": "address" 228 | } 229 | ], 230 | "name": "getReserveCaps", 231 | "outputs": [ 232 | { 233 | "internalType": "uint256", 234 | "name": "borrowCap", 235 | "type": "uint256" 236 | }, 237 | { 238 | "internalType": "uint256", 239 | "name": "supplyCap", 240 | "type": "uint256" 241 | } 242 | ], 243 | "stateMutability": "view", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { 249 | "internalType": "address", 250 | "name": "asset", 251 | "type": "address" 252 | } 253 | ], 254 | "name": "getReserveConfigurationData", 255 | "outputs": [ 256 | { 257 | "internalType": "uint256", 258 | "name": "decimals", 259 | "type": "uint256" 260 | }, 261 | { 262 | "internalType": "uint256", 263 | "name": "ltv", 264 | "type": "uint256" 265 | }, 266 | { 267 | "internalType": "uint256", 268 | "name": "liquidationThreshold", 269 | "type": "uint256" 270 | }, 271 | { 272 | "internalType": "uint256", 273 | "name": "liquidationBonus", 274 | "type": "uint256" 275 | }, 276 | { 277 | "internalType": "uint256", 278 | "name": "reserveFactor", 279 | "type": "uint256" 280 | }, 281 | { 282 | "internalType": "bool", 283 | "name": "usageAsCollateralEnabled", 284 | "type": "bool" 285 | }, 286 | { 287 | "internalType": "bool", 288 | "name": "borrowingEnabled", 289 | "type": "bool" 290 | }, 291 | { 292 | "internalType": "bool", 293 | "name": "stableBorrowRateEnabled", 294 | "type": "bool" 295 | }, 296 | { 297 | "internalType": "bool", 298 | "name": "isActive", 299 | "type": "bool" 300 | }, 301 | { 302 | "internalType": "bool", 303 | "name": "isFrozen", 304 | "type": "bool" 305 | } 306 | ], 307 | "stateMutability": "view", 308 | "type": "function" 309 | }, 310 | { 311 | "inputs": [ 312 | { 313 | "internalType": "address", 314 | "name": "asset", 315 | "type": "address" 316 | } 317 | ], 318 | "name": "getReserveData", 319 | "outputs": [ 320 | { 321 | "internalType": "uint256", 322 | "name": "unbacked", 323 | "type": "uint256" 324 | }, 325 | { 326 | "internalType": "uint256", 327 | "name": "accruedToTreasuryScaled", 328 | "type": "uint256" 329 | }, 330 | { 331 | "internalType": "uint256", 332 | "name": "totalAToken", 333 | "type": "uint256" 334 | }, 335 | { 336 | "internalType": "uint256", 337 | "name": "totalStableDebt", 338 | "type": "uint256" 339 | }, 340 | { 341 | "internalType": "uint256", 342 | "name": "totalVariableDebt", 343 | "type": "uint256" 344 | }, 345 | { 346 | "internalType": "uint256", 347 | "name": "liquidityRate", 348 | "type": "uint256" 349 | }, 350 | { 351 | "internalType": "uint256", 352 | "name": "variableBorrowRate", 353 | "type": "uint256" 354 | }, 355 | { 356 | "internalType": "uint256", 357 | "name": "stableBorrowRate", 358 | "type": "uint256" 359 | }, 360 | { 361 | "internalType": "uint256", 362 | "name": "averageStableBorrowRate", 363 | "type": "uint256" 364 | }, 365 | { 366 | "internalType": "uint256", 367 | "name": "liquidityIndex", 368 | "type": "uint256" 369 | }, 370 | { 371 | "internalType": "uint256", 372 | "name": "variableBorrowIndex", 373 | "type": "uint256" 374 | }, 375 | { 376 | "internalType": "uint40", 377 | "name": "lastUpdateTimestamp", 378 | "type": "uint40" 379 | } 380 | ], 381 | "stateMutability": "view", 382 | "type": "function" 383 | }, 384 | { 385 | "inputs": [ 386 | { 387 | "internalType": "address", 388 | "name": "asset", 389 | "type": "address" 390 | } 391 | ], 392 | "name": "getReserveTokensAddresses", 393 | "outputs": [ 394 | { 395 | "internalType": "address", 396 | "name": "aTokenAddress", 397 | "type": "address" 398 | }, 399 | { 400 | "internalType": "address", 401 | "name": "stableDebtTokenAddress", 402 | "type": "address" 403 | }, 404 | { 405 | "internalType": "address", 406 | "name": "variableDebtTokenAddress", 407 | "type": "address" 408 | } 409 | ], 410 | "stateMutability": "view", 411 | "type": "function" 412 | }, 413 | { 414 | "inputs": [ 415 | { 416 | "internalType": "address", 417 | "name": "asset", 418 | "type": "address" 419 | } 420 | ], 421 | "name": "getSiloedBorrowing", 422 | "outputs": [ 423 | { 424 | "internalType": "bool", 425 | "name": "", 426 | "type": "bool" 427 | } 428 | ], 429 | "stateMutability": "view", 430 | "type": "function" 431 | }, 432 | { 433 | "inputs": [ 434 | { 435 | "internalType": "address", 436 | "name": "asset", 437 | "type": "address" 438 | } 439 | ], 440 | "name": "getTotalDebt", 441 | "outputs": [ 442 | { 443 | "internalType": "uint256", 444 | "name": "", 445 | "type": "uint256" 446 | } 447 | ], 448 | "stateMutability": "view", 449 | "type": "function" 450 | }, 451 | { 452 | "inputs": [ 453 | { 454 | "internalType": "address", 455 | "name": "asset", 456 | "type": "address" 457 | } 458 | ], 459 | "name": "getUnbackedMintCap", 460 | "outputs": [ 461 | { 462 | "internalType": "uint256", 463 | "name": "", 464 | "type": "uint256" 465 | } 466 | ], 467 | "stateMutability": "view", 468 | "type": "function" 469 | }, 470 | { 471 | "inputs": [ 472 | { 473 | "internalType": "address", 474 | "name": "asset", 475 | "type": "address" 476 | }, 477 | { 478 | "internalType": "address", 479 | "name": "user", 480 | "type": "address" 481 | } 482 | ], 483 | "name": "getUserReserveData", 484 | "outputs": [ 485 | { 486 | "internalType": "uint256", 487 | "name": "currentATokenBalance", 488 | "type": "uint256" 489 | }, 490 | { 491 | "internalType": "uint256", 492 | "name": "currentStableDebt", 493 | "type": "uint256" 494 | }, 495 | { 496 | "internalType": "uint256", 497 | "name": "currentVariableDebt", 498 | "type": "uint256" 499 | }, 500 | { 501 | "internalType": "uint256", 502 | "name": "principalStableDebt", 503 | "type": "uint256" 504 | }, 505 | { 506 | "internalType": "uint256", 507 | "name": "scaledVariableDebt", 508 | "type": "uint256" 509 | }, 510 | { 511 | "internalType": "uint256", 512 | "name": "stableBorrowRate", 513 | "type": "uint256" 514 | }, 515 | { 516 | "internalType": "uint256", 517 | "name": "liquidityRate", 518 | "type": "uint256" 519 | }, 520 | { 521 | "internalType": "uint40", 522 | "name": "stableRateLastUpdated", 523 | "type": "uint40" 524 | }, 525 | { 526 | "internalType": "bool", 527 | "name": "usageAsCollateralEnabled", 528 | "type": "bool" 529 | } 530 | ], 531 | "stateMutability": "view", 532 | "type": "function" 533 | }, 534 | { 535 | "inputs": [ 536 | { 537 | "internalType": "address", 538 | "name": "asset", 539 | "type": "address" 540 | } 541 | ], 542 | "name": "getVirtualUnderlyingBalance", 543 | "outputs": [ 544 | { 545 | "internalType": "uint256", 546 | "name": "", 547 | "type": "uint256" 548 | } 549 | ], 550 | "stateMutability": "view", 551 | "type": "function" 552 | } 553 | ] 554 | -------------------------------------------------------------------------------- /abis/aave_v3/UIPoolDataProviderV3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "contract IEACAggregatorProxy", 6 | "name": "_networkBaseTokenPriceInUsdProxyAggregator", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "contract IEACAggregatorProxy", 11 | "name": "_marketReferenceCurrencyPriceInUsdProxyAggregator", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "inputs": [], 20 | "name": "ETH_CURRENCY_UNIT", 21 | "outputs": [ 22 | { 23 | "internalType": "uint256", 24 | "name": "", 25 | "type": "uint256" 26 | } 27 | ], 28 | "stateMutability": "view", 29 | "type": "function" 30 | }, 31 | { 32 | "inputs": [], 33 | "name": "MKR_ADDRESS", 34 | "outputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "", 38 | "type": "address" 39 | } 40 | ], 41 | "stateMutability": "view", 42 | "type": "function" 43 | }, 44 | { 45 | "inputs": [ 46 | { 47 | "internalType": "bytes32", 48 | "name": "_bytes32", 49 | "type": "bytes32" 50 | } 51 | ], 52 | "name": "bytes32ToString", 53 | "outputs": [ 54 | { 55 | "internalType": "string", 56 | "name": "", 57 | "type": "string" 58 | } 59 | ], 60 | "stateMutability": "pure", 61 | "type": "function" 62 | }, 63 | { 64 | "inputs": [ 65 | { 66 | "internalType": "contract IPoolAddressesProvider", 67 | "name": "provider", 68 | "type": "address" 69 | } 70 | ], 71 | "name": "getEModes", 72 | "outputs": [ 73 | { 74 | "components": [ 75 | { 76 | "internalType": "uint8", 77 | "name": "id", 78 | "type": "uint8" 79 | }, 80 | { 81 | "components": [ 82 | { 83 | "internalType": "uint16", 84 | "name": "ltv", 85 | "type": "uint16" 86 | }, 87 | { 88 | "internalType": "uint16", 89 | "name": "liquidationThreshold", 90 | "type": "uint16" 91 | }, 92 | { 93 | "internalType": "uint16", 94 | "name": "liquidationBonus", 95 | "type": "uint16" 96 | }, 97 | { 98 | "internalType": "uint128", 99 | "name": "collateralBitmap", 100 | "type": "uint128" 101 | }, 102 | { 103 | "internalType": "string", 104 | "name": "label", 105 | "type": "string" 106 | }, 107 | { 108 | "internalType": "uint128", 109 | "name": "borrowableBitmap", 110 | "type": "uint128" 111 | } 112 | ], 113 | "internalType": "struct DataTypes.EModeCategory", 114 | "name": "eMode", 115 | "type": "tuple" 116 | } 117 | ], 118 | "internalType": "struct IUiPoolDataProviderV3.Emode[]", 119 | "name": "", 120 | "type": "tuple[]" 121 | } 122 | ], 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [ 128 | { 129 | "internalType": "contract IPoolAddressesProvider", 130 | "name": "provider", 131 | "type": "address" 132 | } 133 | ], 134 | "name": "getReservesData", 135 | "outputs": [ 136 | { 137 | "components": [ 138 | { 139 | "internalType": "address", 140 | "name": "underlyingAsset", 141 | "type": "address" 142 | }, 143 | { 144 | "internalType": "string", 145 | "name": "name", 146 | "type": "string" 147 | }, 148 | { 149 | "internalType": "string", 150 | "name": "symbol", 151 | "type": "string" 152 | }, 153 | { 154 | "internalType": "uint256", 155 | "name": "decimals", 156 | "type": "uint256" 157 | }, 158 | { 159 | "internalType": "uint256", 160 | "name": "baseLTVasCollateral", 161 | "type": "uint256" 162 | }, 163 | { 164 | "internalType": "uint256", 165 | "name": "reserveLiquidationThreshold", 166 | "type": "uint256" 167 | }, 168 | { 169 | "internalType": "uint256", 170 | "name": "reserveLiquidationBonus", 171 | "type": "uint256" 172 | }, 173 | { 174 | "internalType": "uint256", 175 | "name": "reserveFactor", 176 | "type": "uint256" 177 | }, 178 | { 179 | "internalType": "bool", 180 | "name": "usageAsCollateralEnabled", 181 | "type": "bool" 182 | }, 183 | { 184 | "internalType": "bool", 185 | "name": "borrowingEnabled", 186 | "type": "bool" 187 | }, 188 | { 189 | "internalType": "bool", 190 | "name": "isActive", 191 | "type": "bool" 192 | }, 193 | { 194 | "internalType": "bool", 195 | "name": "isFrozen", 196 | "type": "bool" 197 | }, 198 | { 199 | "internalType": "uint128", 200 | "name": "liquidityIndex", 201 | "type": "uint128" 202 | }, 203 | { 204 | "internalType": "uint128", 205 | "name": "variableBorrowIndex", 206 | "type": "uint128" 207 | }, 208 | { 209 | "internalType": "uint128", 210 | "name": "liquidityRate", 211 | "type": "uint128" 212 | }, 213 | { 214 | "internalType": "uint128", 215 | "name": "variableBorrowRate", 216 | "type": "uint128" 217 | }, 218 | { 219 | "internalType": "uint40", 220 | "name": "lastUpdateTimestamp", 221 | "type": "uint40" 222 | }, 223 | { 224 | "internalType": "address", 225 | "name": "aTokenAddress", 226 | "type": "address" 227 | }, 228 | { 229 | "internalType": "address", 230 | "name": "variableDebtTokenAddress", 231 | "type": "address" 232 | }, 233 | { 234 | "internalType": "address", 235 | "name": "interestRateStrategyAddress", 236 | "type": "address" 237 | }, 238 | { 239 | "internalType": "uint256", 240 | "name": "availableLiquidity", 241 | "type": "uint256" 242 | }, 243 | { 244 | "internalType": "uint256", 245 | "name": "totalScaledVariableDebt", 246 | "type": "uint256" 247 | }, 248 | { 249 | "internalType": "uint256", 250 | "name": "priceInMarketReferenceCurrency", 251 | "type": "uint256" 252 | }, 253 | { 254 | "internalType": "address", 255 | "name": "priceOracle", 256 | "type": "address" 257 | }, 258 | { 259 | "internalType": "uint256", 260 | "name": "variableRateSlope1", 261 | "type": "uint256" 262 | }, 263 | { 264 | "internalType": "uint256", 265 | "name": "variableRateSlope2", 266 | "type": "uint256" 267 | }, 268 | { 269 | "internalType": "uint256", 270 | "name": "baseVariableBorrowRate", 271 | "type": "uint256" 272 | }, 273 | { 274 | "internalType": "uint256", 275 | "name": "optimalUsageRatio", 276 | "type": "uint256" 277 | }, 278 | { 279 | "internalType": "bool", 280 | "name": "isPaused", 281 | "type": "bool" 282 | }, 283 | { 284 | "internalType": "bool", 285 | "name": "isSiloedBorrowing", 286 | "type": "bool" 287 | }, 288 | { 289 | "internalType": "uint128", 290 | "name": "accruedToTreasury", 291 | "type": "uint128" 292 | }, 293 | { 294 | "internalType": "uint128", 295 | "name": "unbacked", 296 | "type": "uint128" 297 | }, 298 | { 299 | "internalType": "uint128", 300 | "name": "isolationModeTotalDebt", 301 | "type": "uint128" 302 | }, 303 | { 304 | "internalType": "bool", 305 | "name": "flashLoanEnabled", 306 | "type": "bool" 307 | }, 308 | { 309 | "internalType": "uint256", 310 | "name": "debtCeiling", 311 | "type": "uint256" 312 | }, 313 | { 314 | "internalType": "uint256", 315 | "name": "debtCeilingDecimals", 316 | "type": "uint256" 317 | }, 318 | { 319 | "internalType": "uint256", 320 | "name": "borrowCap", 321 | "type": "uint256" 322 | }, 323 | { 324 | "internalType": "uint256", 325 | "name": "supplyCap", 326 | "type": "uint256" 327 | }, 328 | { 329 | "internalType": "bool", 330 | "name": "borrowableInIsolation", 331 | "type": "bool" 332 | }, 333 | { 334 | "internalType": "bool", 335 | "name": "virtualAccActive", 336 | "type": "bool" 337 | }, 338 | { 339 | "internalType": "uint128", 340 | "name": "virtualUnderlyingBalance", 341 | "type": "uint128" 342 | } 343 | ], 344 | "internalType": "struct IUiPoolDataProviderV3.AggregatedReserveData[]", 345 | "name": "", 346 | "type": "tuple[]" 347 | }, 348 | { 349 | "components": [ 350 | { 351 | "internalType": "uint256", 352 | "name": "marketReferenceCurrencyUnit", 353 | "type": "uint256" 354 | }, 355 | { 356 | "internalType": "int256", 357 | "name": "marketReferenceCurrencyPriceInUsd", 358 | "type": "int256" 359 | }, 360 | { 361 | "internalType": "int256", 362 | "name": "networkBaseTokenPriceInUsd", 363 | "type": "int256" 364 | }, 365 | { 366 | "internalType": "uint8", 367 | "name": "networkBaseTokenPriceDecimals", 368 | "type": "uint8" 369 | } 370 | ], 371 | "internalType": "struct IUiPoolDataProviderV3.BaseCurrencyInfo", 372 | "name": "", 373 | "type": "tuple" 374 | } 375 | ], 376 | "stateMutability": "view", 377 | "type": "function" 378 | }, 379 | { 380 | "inputs": [ 381 | { 382 | "internalType": "contract IPoolAddressesProvider", 383 | "name": "provider", 384 | "type": "address" 385 | } 386 | ], 387 | "name": "getReservesList", 388 | "outputs": [ 389 | { 390 | "internalType": "address[]", 391 | "name": "", 392 | "type": "address[]" 393 | } 394 | ], 395 | "stateMutability": "view", 396 | "type": "function" 397 | }, 398 | { 399 | "inputs": [ 400 | { 401 | "internalType": "contract IPoolAddressesProvider", 402 | "name": "provider", 403 | "type": "address" 404 | }, 405 | { 406 | "internalType": "address", 407 | "name": "user", 408 | "type": "address" 409 | } 410 | ], 411 | "name": "getUserReservesData", 412 | "outputs": [ 413 | { 414 | "components": [ 415 | { 416 | "internalType": "address", 417 | "name": "underlyingAsset", 418 | "type": "address" 419 | }, 420 | { 421 | "internalType": "uint256", 422 | "name": "scaledATokenBalance", 423 | "type": "uint256" 424 | }, 425 | { 426 | "internalType": "bool", 427 | "name": "usageAsCollateralEnabledOnUser", 428 | "type": "bool" 429 | }, 430 | { 431 | "internalType": "uint256", 432 | "name": "scaledVariableDebt", 433 | "type": "uint256" 434 | } 435 | ], 436 | "internalType": "struct IUiPoolDataProviderV3.UserReserveData[]", 437 | "name": "", 438 | "type": "tuple[]" 439 | }, 440 | { 441 | "internalType": "uint8", 442 | "name": "", 443 | "type": "uint8" 444 | } 445 | ], 446 | "stateMutability": "view", 447 | "type": "function" 448 | }, 449 | { 450 | "inputs": [], 451 | "name": "marketReferenceCurrencyPriceInUsdProxyAggregator", 452 | "outputs": [ 453 | { 454 | "internalType": "contract IEACAggregatorProxy", 455 | "name": "", 456 | "type": "address" 457 | } 458 | ], 459 | "stateMutability": "view", 460 | "type": "function" 461 | }, 462 | { 463 | "inputs": [], 464 | "name": "networkBaseTokenPriceInUsdProxyAggregator", 465 | "outputs": [ 466 | { 467 | "internalType": "contract IEACAggregatorProxy", 468 | "name": "", 469 | "type": "address" 470 | } 471 | ], 472 | "stateMutability": "view", 473 | "type": "function" 474 | } 475 | ] 476 | -------------------------------------------------------------------------------- /src/actors/follower.rs: -------------------------------------------------------------------------------- 1 | use crate::contracts; 2 | use crate::utils::norm; 3 | use actix::prelude::*; 4 | use alloy::{primitives::Address, providers::Provider, rpc::types::Filter, sol_types::SolEvent}; 5 | use futures_util::StreamExt; 6 | use tracing::{error, info, warn}; 7 | 8 | use crate::{ 9 | actors::{ 10 | messages::{ 11 | database, 12 | fanatic::{DoSmthWithLiquidationCall, UpdateReservePrice, UpdateReserveUser}, 13 | follower::{SendFanaticAddr, StartListeningForEvents, StartListeningForOraclePrices}, 14 | }, 15 | Database, Fanatic, 16 | }, 17 | configs::FollowerConfig, 18 | consts::RAY, 19 | }; 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct Follower { 23 | provider: P, 24 | filter: Filter, 25 | 26 | provider_addr: Address, 27 | db_addr: Addr, 28 | fanatic_addr: Option>>, 29 | 30 | pool_contract: contracts::aave_v3::PoolContract::PoolContractInstance<(), P>, 31 | datap_contract: contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 32 | 33 | target: String, 34 | } 35 | 36 | impl Actor for Follower

{ 37 | type Context = Context; 38 | 39 | fn started(&mut self, _: &mut Self::Context) { 40 | info!("[started] Follower"); 41 | } 42 | } 43 | 44 | impl Handler> for Follower

{ 45 | type Result = (); 46 | 47 | fn handle(&mut self, msg: SendFanaticAddr

, _: &mut Context) -> Self::Result { 48 | self.fanatic_addr = Some(msg.0); 49 | } 50 | } 51 | 52 | impl Handler for Follower

{ 53 | type Result = (); 54 | 55 | fn handle( 56 | &mut self, 57 | _: StartListeningForOraclePrices, 58 | ctx: &mut Context, 59 | ) -> Self::Result { 60 | self.listen_oracle_prices(ctx); 61 | } 62 | } 63 | 64 | impl Handler for Follower

{ 65 | type Result = (); 66 | 67 | fn handle(&mut self, _: StartListeningForEvents, ctx: &mut Context) -> Self::Result { 68 | self.listen_events(ctx); 69 | } 70 | } 71 | 72 | impl Follower

{ 73 | pub async fn new(config: FollowerConfig

) -> eyre::Result { 74 | let contracts = config 75 | .db_addr 76 | .send(database::GetProtocolContracts(config.target.clone())) 77 | .await??; 78 | 79 | match ( 80 | contracts.get("Pool"), 81 | contracts.get("PoolAddressesProvider"), 82 | contracts.get("UiPoolDataProviderV3"), 83 | ) { 84 | (Some(pool_addr), Some(provider_addr), Some(datap_addr)) => { 85 | let filter = Filter::new().address(*pool_addr).events(vec![ 86 | contracts::aave_v3::PoolContract::LiquidationCall::SIGNATURE, 87 | contracts::aave_v3::PoolContract::Supply::SIGNATURE, 88 | contracts::aave_v3::PoolContract::Borrow::SIGNATURE, 89 | contracts::aave_v3::PoolContract::Repay::SIGNATURE, 90 | contracts::aave_v3::PoolContract::Withdraw::SIGNATURE, 91 | contracts::aave_v3::PoolContract::ReserveDataUpdated::SIGNATURE, 92 | ]); 93 | 94 | let pool_contract = 95 | contracts::aave_v3::PoolContract::new(*pool_addr, config.provider.clone()); 96 | let datap_contract = contracts::aave_v3::DataProviderContract::new( 97 | *datap_addr, 98 | config.provider.clone(), 99 | ); 100 | 101 | Ok(Self { 102 | provider: config.provider, 103 | filter, 104 | db_addr: config.db_addr, 105 | fanatic_addr: None, 106 | provider_addr: *provider_addr, 107 | pool_contract, 108 | datap_contract, 109 | target: config.target.clone(), 110 | }) 111 | } 112 | _ => { 113 | error!("Missing required contract addresses in database"); 114 | Err(eyre::eyre!("Required contract addresses not found")) 115 | } 116 | } 117 | } 118 | 119 | fn listen_oracle_prices(&self, ctx: &mut Context) { 120 | let provider = self.provider.clone(); 121 | let db_addr = self.db_addr.clone(); 122 | let fanatic_addr = self.fanatic_addr.clone(); 123 | let target = self.target.clone(); 124 | 125 | let fut = async move { 126 | // Get the list of aggregator addresses to monitor. 127 | let aggregators = db_addr 128 | .send(database::GetAggregatorMapping(target.clone())) 129 | .await 130 | .unwrap(); 131 | info!( 132 | ?aggregators, 133 | "listening for oracle price events from aggregators [{}] [aggregator => reserve]", 134 | aggregators.keys().len() 135 | ); 136 | 137 | // Build a filter for events coming from the aggregator addresses. 138 | // In this example we filter for logs matching the AnswerUpdated event signature 139 | let filter = Filter::new() 140 | .address(aggregators.keys().cloned().collect::>()) 141 | .events(vec![ 142 | contracts::chainlink::EACAggregatorProxyContract::AnswerUpdated::SIGNATURE, 143 | ]); 144 | 145 | let sub = provider.subscribe_logs(&filter).await.unwrap(); 146 | let mut stream = sub.into_stream(); 147 | 148 | while let Some(log) = stream.next().await { 149 | if let Ok(event) = 150 | contracts::chainlink::EACAggregatorProxyContract::AnswerUpdated::decode_log( 151 | &log.inner, true, 152 | ) 153 | { 154 | let price = oracle_price(&provider, event.address).await; 155 | info!(aggregator=?event.address, ?price, "new price from aggregator"); 156 | 157 | // Update the DB reserve price, and send a message to calculate the affected 158 | // users' health_factor. 159 | // TODO: perhaps this should be done in the `fanatic` actor 160 | // 161 | // get the reserve from the aggregator => reserve HashMap 162 | if let Some(reserve) = aggregators.get(&event.address) { 163 | db_addr 164 | .send(database::UpdateOraclePrice { 165 | reserve: *reserve, 166 | target: target.clone(), 167 | price, 168 | }) 169 | .await 170 | .unwrap() 171 | .unwrap(); 172 | 173 | fanatic_addr 174 | .clone() 175 | .expect("no fanatic_addr found") 176 | .send(UpdateReservePrice { 177 | reserve: *reserve, 178 | new_price: price, 179 | }) 180 | .await 181 | .unwrap(); 182 | } 183 | } 184 | } 185 | }; 186 | 187 | ctx.spawn(fut.into_actor(self)); 188 | } 189 | 190 | /// Listen for realtime action happening in the lending pools 191 | /// ..and act accordingly 192 | fn listen_events(&self, ctx: &mut Context) { 193 | let provider = self.provider.clone(); 194 | let filter = self.filter.clone(); 195 | 196 | let db_addr = self.db_addr.clone(); 197 | let fanatic_addr = self.fanatic_addr.clone(); 198 | 199 | let fut = async move { 200 | let sub = provider.subscribe_logs(&filter).await.unwrap(); 201 | let mut stream = sub.into_stream(); 202 | 203 | while let Some(log) = stream.next().await { 204 | let signature = log.topic0().unwrap(); 205 | 206 | match signature { 207 | hash if *hash 208 | == contracts::aave_v3::PoolContract::LiquidationCall::SIGNATURE_HASH => 209 | { 210 | if let Ok(event) = 211 | contracts::aave_v3::PoolContract::LiquidationCall::decode_log( 212 | &log.inner, true, 213 | ) 214 | { 215 | info!(?event.user, "liquidation_event_handler"); 216 | fanatic_addr 217 | .clone() 218 | .expect("no fanatic_addr found") 219 | .send(DoSmthWithLiquidationCall(event.data)) 220 | .await 221 | .unwrap() 222 | .unwrap(); 223 | } 224 | } 225 | hash if *hash == contracts::aave_v3::PoolContract::Supply::SIGNATURE_HASH => { 226 | if let Ok(event) = 227 | contracts::aave_v3::PoolContract::Supply::decode_log(&log.inner, true) 228 | { 229 | info!(reserve = ?event.reserve, user = ?event.user, amount = ?event.amount, "supply_event_handler"); 230 | fanatic_addr 231 | .clone() 232 | .expect("no fanatic_addr found") 233 | .send(UpdateReserveUser { 234 | reserve: event.reserve, 235 | user_addr: event.user, 236 | }) 237 | .await 238 | .unwrap(); 239 | } 240 | } 241 | hash if *hash == contracts::aave_v3::PoolContract::Borrow::SIGNATURE_HASH => { 242 | if let Ok(event) = 243 | contracts::aave_v3::PoolContract::Borrow::decode_log(&log.inner, true) 244 | { 245 | info!(reserve = ?event.reserve, user = ?event.user, amount = ?event.amount, "borrow_event_handler"); 246 | fanatic_addr 247 | .clone() 248 | .expect("no fanatic_addr found") 249 | .send(UpdateReserveUser { 250 | reserve: event.reserve, 251 | user_addr: event.user, 252 | }) 253 | .await 254 | .unwrap(); 255 | } 256 | } 257 | hash if *hash == contracts::aave_v3::PoolContract::Repay::SIGNATURE_HASH => { 258 | if let Ok(event) = 259 | contracts::aave_v3::PoolContract::Repay::decode_log(&log.inner, true) 260 | { 261 | info!(reserve = ?event.reserve, user = ?event.user, amount = ?event.amount, "repay_event_handler"); 262 | fanatic_addr 263 | .clone() 264 | .expect("no fanatic_addr found") 265 | .send(UpdateReserveUser { 266 | reserve: event.reserve, 267 | user_addr: event.user, 268 | }) 269 | .await 270 | .unwrap(); 271 | } 272 | } 273 | hash if *hash == contracts::aave_v3::PoolContract::Withdraw::SIGNATURE_HASH => { 274 | if let Ok(event) = 275 | contracts::aave_v3::PoolContract::Withdraw::decode_log(&log.inner, true) 276 | { 277 | info!(reserve = ?event.reserve, user = ?event.user, amount = ?event.amount, "withdraw_event_handler"); 278 | fanatic_addr 279 | .clone() 280 | .expect("no fanatic_addr found") 281 | .send(UpdateReserveUser { 282 | reserve: event.reserve, 283 | user_addr: event.user, 284 | }) 285 | .await 286 | .unwrap(); 287 | } 288 | } 289 | hash if *hash 290 | == contracts::aave_v3::PoolContract::ReserveDataUpdated::SIGNATURE_HASH => 291 | { 292 | if let Ok(event) = 293 | contracts::aave_v3::PoolContract::ReserveDataUpdated::decode_log( 294 | &log.inner, true, 295 | ) 296 | { 297 | info!(reserve = ?event.reserve, liq_rate = ?event.liquidityRate, 298 | liq_index = ?event.liquidityIndex, stable_borrow_rate = ?event.stableBorrowRate, 299 | variable_borrow_rate = ?event.variableBorrowRate, "reserve_update_event_handler"); 300 | match db_addr 301 | .send(database::UpsertReservesStats(vec![ 302 | database::UpsertReserveStats { 303 | reserve: event.reserve.to_string(), 304 | liquidity_rate: norm( 305 | event.liquidityRate, 306 | Some(100.0 / RAY), 307 | ) 308 | .unwrap(), 309 | variable_borrow_rate: norm( 310 | event.variableBorrowRate, 311 | Some(100.0 / RAY), 312 | ) 313 | .unwrap(), 314 | liquidity_index: norm( 315 | event.liquidityIndex, 316 | Some(1.0 / RAY), 317 | ) 318 | .unwrap(), 319 | variable_borrow_index: norm( 320 | event.variableBorrowIndex, 321 | Some(1.0 / RAY), 322 | ) 323 | .unwrap(), 324 | }, 325 | ])) 326 | .await 327 | { 328 | Ok(Ok(_)) => (), 329 | Ok(Err(e)) => { 330 | error!(?event.reserve, error = ?e, "Failed to update reserve data") 331 | } 332 | Err(e) => { 333 | error!(?event.reserve, error = ?e, "Failed to send reserve update") 334 | } 335 | } 336 | } 337 | } 338 | _ => warn!(?signature, ?log, "unknown event"), 339 | } 340 | } 341 | }; 342 | 343 | ctx.spawn(fut.into_actor(self)); 344 | } 345 | } 346 | 347 | pub async fn oracle_price(provider: P, addr: Address) -> f64 { 348 | let contract = contracts::chainlink::OffchainAggregatorContract::new(addr, &provider); 349 | 350 | let latest_answer = contract 351 | .latestAnswer() 352 | .call() 353 | .await 354 | .unwrap() 355 | ._0 356 | .to_string() 357 | .parse::() 358 | .unwrap(); 359 | 360 | // Try to get decimals using CLRatePriceCapAdapter, if it fails, use CLSynchronicityPriceAdapterPegToBase. 361 | let decimals = match contract.decimals().call().await { 362 | Ok(resp) => resp._0, 363 | Err(e) => { 364 | warn!(error = ?e, "Failed to get decimals from CLRatePriceCapAdapter, trying CLSynchronicityPriceAdapterPegToBase"); 365 | let synch_adapter = 366 | contracts::chainlink::CLSynchronicityPriceAdapterPegToBaseContract::new( 367 | addr, &provider, 368 | ); 369 | synch_adapter.DECIMALS().call().await.unwrap()._0 370 | } 371 | }; 372 | 373 | latest_answer / 10_f64.powi(decimals as i32) 374 | } 375 | -------------------------------------------------------------------------------- /abis/uniswap_v3/UniswapV3Pool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "address", 13 | "name": "owner", 14 | "type": "address" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "int24", 19 | "name": "tickLower", 20 | "type": "int24" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "int24", 25 | "name": "tickUpper", 26 | "type": "int24" 27 | }, 28 | { 29 | "indexed": false, 30 | "internalType": "uint128", 31 | "name": "amount", 32 | "type": "uint128" 33 | }, 34 | { 35 | "indexed": false, 36 | "internalType": "uint256", 37 | "name": "amount0", 38 | "type": "uint256" 39 | }, 40 | { 41 | "indexed": false, 42 | "internalType": "uint256", 43 | "name": "amount1", 44 | "type": "uint256" 45 | } 46 | ], 47 | "name": "Burn", 48 | "type": "event" 49 | }, 50 | { 51 | "anonymous": false, 52 | "inputs": [ 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "owner", 57 | "type": "address" 58 | }, 59 | { 60 | "indexed": false, 61 | "internalType": "address", 62 | "name": "recipient", 63 | "type": "address" 64 | }, 65 | { 66 | "indexed": true, 67 | "internalType": "int24", 68 | "name": "tickLower", 69 | "type": "int24" 70 | }, 71 | { 72 | "indexed": true, 73 | "internalType": "int24", 74 | "name": "tickUpper", 75 | "type": "int24" 76 | }, 77 | { 78 | "indexed": false, 79 | "internalType": "uint128", 80 | "name": "amount0", 81 | "type": "uint128" 82 | }, 83 | { 84 | "indexed": false, 85 | "internalType": "uint128", 86 | "name": "amount1", 87 | "type": "uint128" 88 | } 89 | ], 90 | "name": "Collect", 91 | "type": "event" 92 | }, 93 | { 94 | "anonymous": false, 95 | "inputs": [ 96 | { 97 | "indexed": true, 98 | "internalType": "address", 99 | "name": "sender", 100 | "type": "address" 101 | }, 102 | { 103 | "indexed": true, 104 | "internalType": "address", 105 | "name": "recipient", 106 | "type": "address" 107 | }, 108 | { 109 | "indexed": false, 110 | "internalType": "uint128", 111 | "name": "amount0", 112 | "type": "uint128" 113 | }, 114 | { 115 | "indexed": false, 116 | "internalType": "uint128", 117 | "name": "amount1", 118 | "type": "uint128" 119 | } 120 | ], 121 | "name": "CollectProtocol", 122 | "type": "event" 123 | }, 124 | { 125 | "anonymous": false, 126 | "inputs": [ 127 | { 128 | "indexed": true, 129 | "internalType": "address", 130 | "name": "sender", 131 | "type": "address" 132 | }, 133 | { 134 | "indexed": true, 135 | "internalType": "address", 136 | "name": "recipient", 137 | "type": "address" 138 | }, 139 | { 140 | "indexed": false, 141 | "internalType": "uint256", 142 | "name": "amount0", 143 | "type": "uint256" 144 | }, 145 | { 146 | "indexed": false, 147 | "internalType": "uint256", 148 | "name": "amount1", 149 | "type": "uint256" 150 | }, 151 | { 152 | "indexed": false, 153 | "internalType": "uint256", 154 | "name": "paid0", 155 | "type": "uint256" 156 | }, 157 | { 158 | "indexed": false, 159 | "internalType": "uint256", 160 | "name": "paid1", 161 | "type": "uint256" 162 | } 163 | ], 164 | "name": "Flash", 165 | "type": "event" 166 | }, 167 | { 168 | "anonymous": false, 169 | "inputs": [ 170 | { 171 | "indexed": false, 172 | "internalType": "uint16", 173 | "name": "observationCardinalityNextOld", 174 | "type": "uint16" 175 | }, 176 | { 177 | "indexed": false, 178 | "internalType": "uint16", 179 | "name": "observationCardinalityNextNew", 180 | "type": "uint16" 181 | } 182 | ], 183 | "name": "IncreaseObservationCardinalityNext", 184 | "type": "event" 185 | }, 186 | { 187 | "anonymous": false, 188 | "inputs": [ 189 | { 190 | "indexed": false, 191 | "internalType": "uint160", 192 | "name": "sqrtPriceX96", 193 | "type": "uint160" 194 | }, 195 | { 196 | "indexed": false, 197 | "internalType": "int24", 198 | "name": "tick", 199 | "type": "int24" 200 | } 201 | ], 202 | "name": "Initialize", 203 | "type": "event" 204 | }, 205 | { 206 | "anonymous": false, 207 | "inputs": [ 208 | { 209 | "indexed": false, 210 | "internalType": "address", 211 | "name": "sender", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": true, 216 | "internalType": "address", 217 | "name": "owner", 218 | "type": "address" 219 | }, 220 | { 221 | "indexed": true, 222 | "internalType": "int24", 223 | "name": "tickLower", 224 | "type": "int24" 225 | }, 226 | { 227 | "indexed": true, 228 | "internalType": "int24", 229 | "name": "tickUpper", 230 | "type": "int24" 231 | }, 232 | { 233 | "indexed": false, 234 | "internalType": "uint128", 235 | "name": "amount", 236 | "type": "uint128" 237 | }, 238 | { 239 | "indexed": false, 240 | "internalType": "uint256", 241 | "name": "amount0", 242 | "type": "uint256" 243 | }, 244 | { 245 | "indexed": false, 246 | "internalType": "uint256", 247 | "name": "amount1", 248 | "type": "uint256" 249 | } 250 | ], 251 | "name": "Mint", 252 | "type": "event" 253 | }, 254 | { 255 | "anonymous": false, 256 | "inputs": [ 257 | { 258 | "indexed": false, 259 | "internalType": "uint8", 260 | "name": "feeProtocol0Old", 261 | "type": "uint8" 262 | }, 263 | { 264 | "indexed": false, 265 | "internalType": "uint8", 266 | "name": "feeProtocol1Old", 267 | "type": "uint8" 268 | }, 269 | { 270 | "indexed": false, 271 | "internalType": "uint8", 272 | "name": "feeProtocol0New", 273 | "type": "uint8" 274 | }, 275 | { 276 | "indexed": false, 277 | "internalType": "uint8", 278 | "name": "feeProtocol1New", 279 | "type": "uint8" 280 | } 281 | ], 282 | "name": "SetFeeProtocol", 283 | "type": "event" 284 | }, 285 | { 286 | "anonymous": false, 287 | "inputs": [ 288 | { 289 | "indexed": true, 290 | "internalType": "address", 291 | "name": "sender", 292 | "type": "address" 293 | }, 294 | { 295 | "indexed": true, 296 | "internalType": "address", 297 | "name": "recipient", 298 | "type": "address" 299 | }, 300 | { 301 | "indexed": false, 302 | "internalType": "int256", 303 | "name": "amount0", 304 | "type": "int256" 305 | }, 306 | { 307 | "indexed": false, 308 | "internalType": "int256", 309 | "name": "amount1", 310 | "type": "int256" 311 | }, 312 | { 313 | "indexed": false, 314 | "internalType": "uint160", 315 | "name": "sqrtPriceX96", 316 | "type": "uint160" 317 | }, 318 | { 319 | "indexed": false, 320 | "internalType": "uint128", 321 | "name": "liquidity", 322 | "type": "uint128" 323 | }, 324 | { 325 | "indexed": false, 326 | "internalType": "int24", 327 | "name": "tick", 328 | "type": "int24" 329 | } 330 | ], 331 | "name": "Swap", 332 | "type": "event" 333 | }, 334 | { 335 | "inputs": [ 336 | { 337 | "internalType": "int24", 338 | "name": "tickLower", 339 | "type": "int24" 340 | }, 341 | { 342 | "internalType": "int24", 343 | "name": "tickUpper", 344 | "type": "int24" 345 | }, 346 | { 347 | "internalType": "uint128", 348 | "name": "amount", 349 | "type": "uint128" 350 | } 351 | ], 352 | "name": "burn", 353 | "outputs": [ 354 | { 355 | "internalType": "uint256", 356 | "name": "amount0", 357 | "type": "uint256" 358 | }, 359 | { 360 | "internalType": "uint256", 361 | "name": "amount1", 362 | "type": "uint256" 363 | } 364 | ], 365 | "stateMutability": "nonpayable", 366 | "type": "function" 367 | }, 368 | { 369 | "inputs": [ 370 | { 371 | "internalType": "address", 372 | "name": "recipient", 373 | "type": "address" 374 | }, 375 | { 376 | "internalType": "int24", 377 | "name": "tickLower", 378 | "type": "int24" 379 | }, 380 | { 381 | "internalType": "int24", 382 | "name": "tickUpper", 383 | "type": "int24" 384 | }, 385 | { 386 | "internalType": "uint128", 387 | "name": "amount0Requested", 388 | "type": "uint128" 389 | }, 390 | { 391 | "internalType": "uint128", 392 | "name": "amount1Requested", 393 | "type": "uint128" 394 | } 395 | ], 396 | "name": "collect", 397 | "outputs": [ 398 | { 399 | "internalType": "uint128", 400 | "name": "amount0", 401 | "type": "uint128" 402 | }, 403 | { 404 | "internalType": "uint128", 405 | "name": "amount1", 406 | "type": "uint128" 407 | } 408 | ], 409 | "stateMutability": "nonpayable", 410 | "type": "function" 411 | }, 412 | { 413 | "inputs": [ 414 | { 415 | "internalType": "address", 416 | "name": "recipient", 417 | "type": "address" 418 | }, 419 | { 420 | "internalType": "uint128", 421 | "name": "amount0Requested", 422 | "type": "uint128" 423 | }, 424 | { 425 | "internalType": "uint128", 426 | "name": "amount1Requested", 427 | "type": "uint128" 428 | } 429 | ], 430 | "name": "collectProtocol", 431 | "outputs": [ 432 | { 433 | "internalType": "uint128", 434 | "name": "amount0", 435 | "type": "uint128" 436 | }, 437 | { 438 | "internalType": "uint128", 439 | "name": "amount1", 440 | "type": "uint128" 441 | } 442 | ], 443 | "stateMutability": "nonpayable", 444 | "type": "function" 445 | }, 446 | { 447 | "inputs": [], 448 | "name": "factory", 449 | "outputs": [ 450 | { 451 | "internalType": "address", 452 | "name": "", 453 | "type": "address" 454 | } 455 | ], 456 | "stateMutability": "view", 457 | "type": "function" 458 | }, 459 | { 460 | "inputs": [], 461 | "name": "fee", 462 | "outputs": [ 463 | { 464 | "internalType": "uint24", 465 | "name": "", 466 | "type": "uint24" 467 | } 468 | ], 469 | "stateMutability": "view", 470 | "type": "function" 471 | }, 472 | { 473 | "inputs": [], 474 | "name": "feeGrowthGlobal0X128", 475 | "outputs": [ 476 | { 477 | "internalType": "uint256", 478 | "name": "", 479 | "type": "uint256" 480 | } 481 | ], 482 | "stateMutability": "view", 483 | "type": "function" 484 | }, 485 | { 486 | "inputs": [], 487 | "name": "feeGrowthGlobal1X128", 488 | "outputs": [ 489 | { 490 | "internalType": "uint256", 491 | "name": "", 492 | "type": "uint256" 493 | } 494 | ], 495 | "stateMutability": "view", 496 | "type": "function" 497 | }, 498 | { 499 | "inputs": [ 500 | { 501 | "internalType": "address", 502 | "name": "recipient", 503 | "type": "address" 504 | }, 505 | { 506 | "internalType": "uint256", 507 | "name": "amount0", 508 | "type": "uint256" 509 | }, 510 | { 511 | "internalType": "uint256", 512 | "name": "amount1", 513 | "type": "uint256" 514 | }, 515 | { 516 | "internalType": "bytes", 517 | "name": "data", 518 | "type": "bytes" 519 | } 520 | ], 521 | "name": "flash", 522 | "outputs": [], 523 | "stateMutability": "nonpayable", 524 | "type": "function" 525 | }, 526 | { 527 | "inputs": [ 528 | { 529 | "internalType": "uint16", 530 | "name": "observationCardinalityNext", 531 | "type": "uint16" 532 | } 533 | ], 534 | "name": "increaseObservationCardinalityNext", 535 | "outputs": [], 536 | "stateMutability": "nonpayable", 537 | "type": "function" 538 | }, 539 | { 540 | "inputs": [ 541 | { 542 | "internalType": "uint160", 543 | "name": "sqrtPriceX96", 544 | "type": "uint160" 545 | } 546 | ], 547 | "name": "initialize", 548 | "outputs": [], 549 | "stateMutability": "nonpayable", 550 | "type": "function" 551 | }, 552 | { 553 | "inputs": [], 554 | "name": "liquidity", 555 | "outputs": [ 556 | { 557 | "internalType": "uint128", 558 | "name": "", 559 | "type": "uint128" 560 | } 561 | ], 562 | "stateMutability": "view", 563 | "type": "function" 564 | }, 565 | { 566 | "inputs": [], 567 | "name": "maxLiquidityPerTick", 568 | "outputs": [ 569 | { 570 | "internalType": "uint128", 571 | "name": "", 572 | "type": "uint128" 573 | } 574 | ], 575 | "stateMutability": "view", 576 | "type": "function" 577 | }, 578 | { 579 | "inputs": [ 580 | { 581 | "internalType": "address", 582 | "name": "recipient", 583 | "type": "address" 584 | }, 585 | { 586 | "internalType": "int24", 587 | "name": "tickLower", 588 | "type": "int24" 589 | }, 590 | { 591 | "internalType": "int24", 592 | "name": "tickUpper", 593 | "type": "int24" 594 | }, 595 | { 596 | "internalType": "uint128", 597 | "name": "amount", 598 | "type": "uint128" 599 | }, 600 | { 601 | "internalType": "bytes", 602 | "name": "data", 603 | "type": "bytes" 604 | } 605 | ], 606 | "name": "mint", 607 | "outputs": [ 608 | { 609 | "internalType": "uint256", 610 | "name": "amount0", 611 | "type": "uint256" 612 | }, 613 | { 614 | "internalType": "uint256", 615 | "name": "amount1", 616 | "type": "uint256" 617 | } 618 | ], 619 | "stateMutability": "nonpayable", 620 | "type": "function" 621 | }, 622 | { 623 | "inputs": [ 624 | { 625 | "internalType": "uint256", 626 | "name": "", 627 | "type": "uint256" 628 | } 629 | ], 630 | "name": "observations", 631 | "outputs": [ 632 | { 633 | "internalType": "uint32", 634 | "name": "blockTimestamp", 635 | "type": "uint32" 636 | }, 637 | { 638 | "internalType": "int56", 639 | "name": "tickCumulative", 640 | "type": "int56" 641 | }, 642 | { 643 | "internalType": "uint160", 644 | "name": "secondsPerLiquidityCumulativeX128", 645 | "type": "uint160" 646 | }, 647 | { 648 | "internalType": "bool", 649 | "name": "initialized", 650 | "type": "bool" 651 | } 652 | ], 653 | "stateMutability": "view", 654 | "type": "function" 655 | }, 656 | { 657 | "inputs": [ 658 | { 659 | "internalType": "uint32[]", 660 | "name": "secondsAgos", 661 | "type": "uint32[]" 662 | } 663 | ], 664 | "name": "observe", 665 | "outputs": [ 666 | { 667 | "internalType": "int56[]", 668 | "name": "tickCumulatives", 669 | "type": "int56[]" 670 | }, 671 | { 672 | "internalType": "uint160[]", 673 | "name": "secondsPerLiquidityCumulativeX128s", 674 | "type": "uint160[]" 675 | } 676 | ], 677 | "stateMutability": "view", 678 | "type": "function" 679 | }, 680 | { 681 | "inputs": [ 682 | { 683 | "internalType": "bytes32", 684 | "name": "", 685 | "type": "bytes32" 686 | } 687 | ], 688 | "name": "positions", 689 | "outputs": [ 690 | { 691 | "internalType": "uint128", 692 | "name": "liquidity", 693 | "type": "uint128" 694 | }, 695 | { 696 | "internalType": "uint256", 697 | "name": "feeGrowthInside0LastX128", 698 | "type": "uint256" 699 | }, 700 | { 701 | "internalType": "uint256", 702 | "name": "feeGrowthInside1LastX128", 703 | "type": "uint256" 704 | }, 705 | { 706 | "internalType": "uint128", 707 | "name": "tokensOwed0", 708 | "type": "uint128" 709 | }, 710 | { 711 | "internalType": "uint128", 712 | "name": "tokensOwed1", 713 | "type": "uint128" 714 | } 715 | ], 716 | "stateMutability": "view", 717 | "type": "function" 718 | }, 719 | { 720 | "inputs": [], 721 | "name": "protocolFees", 722 | "outputs": [ 723 | { 724 | "internalType": "uint128", 725 | "name": "token0", 726 | "type": "uint128" 727 | }, 728 | { 729 | "internalType": "uint128", 730 | "name": "token1", 731 | "type": "uint128" 732 | } 733 | ], 734 | "stateMutability": "view", 735 | "type": "function" 736 | }, 737 | { 738 | "inputs": [ 739 | { 740 | "internalType": "uint8", 741 | "name": "feeProtocol0", 742 | "type": "uint8" 743 | }, 744 | { 745 | "internalType": "uint8", 746 | "name": "feeProtocol1", 747 | "type": "uint8" 748 | } 749 | ], 750 | "name": "setFeeProtocol", 751 | "outputs": [], 752 | "stateMutability": "nonpayable", 753 | "type": "function" 754 | }, 755 | { 756 | "inputs": [], 757 | "name": "slot0", 758 | "outputs": [ 759 | { 760 | "internalType": "uint160", 761 | "name": "sqrtPriceX96", 762 | "type": "uint160" 763 | }, 764 | { 765 | "internalType": "int24", 766 | "name": "tick", 767 | "type": "int24" 768 | }, 769 | { 770 | "internalType": "uint16", 771 | "name": "observationIndex", 772 | "type": "uint16" 773 | }, 774 | { 775 | "internalType": "uint16", 776 | "name": "observationCardinality", 777 | "type": "uint16" 778 | }, 779 | { 780 | "internalType": "uint16", 781 | "name": "observationCardinalityNext", 782 | "type": "uint16" 783 | }, 784 | { 785 | "internalType": "uint8", 786 | "name": "feeProtocol", 787 | "type": "uint8" 788 | }, 789 | { 790 | "internalType": "bool", 791 | "name": "unlocked", 792 | "type": "bool" 793 | } 794 | ], 795 | "stateMutability": "view", 796 | "type": "function" 797 | }, 798 | { 799 | "inputs": [ 800 | { 801 | "internalType": "int24", 802 | "name": "tickLower", 803 | "type": "int24" 804 | }, 805 | { 806 | "internalType": "int24", 807 | "name": "tickUpper", 808 | "type": "int24" 809 | } 810 | ], 811 | "name": "snapshotCumulativesInside", 812 | "outputs": [ 813 | { 814 | "internalType": "int56", 815 | "name": "tickCumulativeInside", 816 | "type": "int56" 817 | }, 818 | { 819 | "internalType": "uint160", 820 | "name": "secondsPerLiquidityInsideX128", 821 | "type": "uint160" 822 | }, 823 | { 824 | "internalType": "uint32", 825 | "name": "secondsInside", 826 | "type": "uint32" 827 | } 828 | ], 829 | "stateMutability": "view", 830 | "type": "function" 831 | }, 832 | { 833 | "inputs": [ 834 | { 835 | "internalType": "address", 836 | "name": "recipient", 837 | "type": "address" 838 | }, 839 | { 840 | "internalType": "bool", 841 | "name": "zeroForOne", 842 | "type": "bool" 843 | }, 844 | { 845 | "internalType": "int256", 846 | "name": "amountSpecified", 847 | "type": "int256" 848 | }, 849 | { 850 | "internalType": "uint160", 851 | "name": "sqrtPriceLimitX96", 852 | "type": "uint160" 853 | }, 854 | { 855 | "internalType": "bytes", 856 | "name": "data", 857 | "type": "bytes" 858 | } 859 | ], 860 | "name": "swap", 861 | "outputs": [ 862 | { 863 | "internalType": "int256", 864 | "name": "amount0", 865 | "type": "int256" 866 | }, 867 | { 868 | "internalType": "int256", 869 | "name": "amount1", 870 | "type": "int256" 871 | } 872 | ], 873 | "stateMutability": "nonpayable", 874 | "type": "function" 875 | }, 876 | { 877 | "inputs": [ 878 | { 879 | "internalType": "int16", 880 | "name": "", 881 | "type": "int16" 882 | } 883 | ], 884 | "name": "tickBitmap", 885 | "outputs": [ 886 | { 887 | "internalType": "uint256", 888 | "name": "", 889 | "type": "uint256" 890 | } 891 | ], 892 | "stateMutability": "view", 893 | "type": "function" 894 | }, 895 | { 896 | "inputs": [], 897 | "name": "tickSpacing", 898 | "outputs": [ 899 | { 900 | "internalType": "int24", 901 | "name": "", 902 | "type": "int24" 903 | } 904 | ], 905 | "stateMutability": "view", 906 | "type": "function" 907 | }, 908 | { 909 | "inputs": [ 910 | { 911 | "internalType": "int24", 912 | "name": "", 913 | "type": "int24" 914 | } 915 | ], 916 | "name": "ticks", 917 | "outputs": [ 918 | { 919 | "internalType": "uint128", 920 | "name": "liquidityGross", 921 | "type": "uint128" 922 | }, 923 | { 924 | "internalType": "int128", 925 | "name": "liquidityNet", 926 | "type": "int128" 927 | }, 928 | { 929 | "internalType": "uint256", 930 | "name": "feeGrowthOutside0X128", 931 | "type": "uint256" 932 | }, 933 | { 934 | "internalType": "uint256", 935 | "name": "feeGrowthOutside1X128", 936 | "type": "uint256" 937 | }, 938 | { 939 | "internalType": "int56", 940 | "name": "tickCumulativeOutside", 941 | "type": "int56" 942 | }, 943 | { 944 | "internalType": "uint160", 945 | "name": "secondsPerLiquidityOutsideX128", 946 | "type": "uint160" 947 | }, 948 | { 949 | "internalType": "uint32", 950 | "name": "secondsOutside", 951 | "type": "uint32" 952 | }, 953 | { 954 | "internalType": "bool", 955 | "name": "initialized", 956 | "type": "bool" 957 | } 958 | ], 959 | "stateMutability": "view", 960 | "type": "function" 961 | }, 962 | { 963 | "inputs": [], 964 | "name": "token0", 965 | "outputs": [ 966 | { 967 | "internalType": "address", 968 | "name": "", 969 | "type": "address" 970 | } 971 | ], 972 | "stateMutability": "view", 973 | "type": "function" 974 | }, 975 | { 976 | "inputs": [], 977 | "name": "token1", 978 | "outputs": [ 979 | { 980 | "internalType": "address", 981 | "name": "", 982 | "type": "address" 983 | } 984 | ], 985 | "stateMutability": "view", 986 | "type": "function" 987 | } 988 | ] 989 | -------------------------------------------------------------------------------- /src/actors/fanatic.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use crate::actors::messages::executor::LiquidationRequest; 4 | use crate::actors::Database; 5 | use crate::contracts; 6 | use crate::utils::{health_factor, norm, user_positions}; 7 | use actix::prelude::*; 8 | use alloy::{primitives::Address, providers::Provider}; 9 | use sqlx::types::time::OffsetDateTime; 10 | use tokio::{sync::Mutex, time::Duration}; 11 | use tracing::{error, info, warn}; 12 | 13 | use super::messages::fanatic::SendExecutorAddr; 14 | use super::Executor; 15 | use super::{ 16 | follower::oracle_price, 17 | messages::fanatic::{FailedLiquidation, SuccessfulLiquidation}, 18 | }; 19 | use crate::{ 20 | actors::{ 21 | messages::{ 22 | database, 23 | fanatic::{DoSmthWithLiquidationCall, UpdateReservePrice, UpdateReserveUser}, 24 | follower::{SendFanaticAddr, StartListeningForEvents, StartListeningForOraclePrices}, 25 | }, 26 | Follower, 27 | }, 28 | configs::FanaticConfig, 29 | consts::RAY, 30 | run::Shutdown, 31 | }; 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct Fanatic { 35 | provider: P, 36 | 37 | db_addr: Addr, 38 | follower_addr: Addr>, 39 | executor_addr: Option>>, 40 | 41 | pool_contract: contracts::aave_v3::PoolContract::PoolContractInstance<(), P>, 42 | datap_contract: contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 43 | addressp_contract: 44 | contracts::aave_v3::AddressProviderContract::AddressProviderContractInstance<(), P>, 45 | 46 | users: Arc>>, 47 | reserves: Arc>>, 48 | 49 | target: String, 50 | protocol_details_id: i32, 51 | } 52 | 53 | impl Actor for Fanatic

{ 54 | type Context = Context; 55 | 56 | fn started(&mut self, ctx: &mut Self::Context) { 57 | let addr = ctx.address(); 58 | let follower_addr = self.follower_addr.clone(); 59 | 60 | let fut = async move { 61 | follower_addr.send(SendFanaticAddr(addr)).await.unwrap(); 62 | follower_addr 63 | .send(StartListeningForOraclePrices) 64 | .await 65 | .unwrap(); 66 | follower_addr.send(StartListeningForEvents).await.unwrap(); 67 | }; 68 | 69 | ctx.spawn(fut.into_actor(self)); 70 | 71 | ctx.run_interval(Duration::from_secs(60), |actor, ctx| { 72 | let users = actor.users.clone(); 73 | let db_addr = actor.db_addr.clone(); 74 | let datap_contract = actor.datap_contract.clone(); 75 | let addressp_address = *actor.addressp_contract.address(); 76 | let protocol_details_id = actor.protocol_details_id; 77 | let target = actor.target.clone(); 78 | 79 | let fut = async move { 80 | if let Err(e) = update_recent_users( 81 | &users, 82 | &db_addr, 83 | protocol_details_id, 84 | &datap_contract, 85 | &addressp_address, 86 | target, 87 | ) 88 | .await 89 | { 90 | error!("Failed to update recent users: {}", e); 91 | } 92 | }; 93 | 94 | ctx.spawn(fut.into_actor(actor)); 95 | }); 96 | } 97 | } 98 | 99 | impl Handler> for Fanatic

{ 100 | type Result = (); 101 | 102 | fn handle(&mut self, msg: SendExecutorAddr

, _: &mut Context) -> Self::Result { 103 | self.executor_addr = Some(msg.0); 104 | } 105 | } 106 | 107 | async fn update_recent_users( 108 | users: &Arc>>, 109 | db_addr: &Addr, 110 | protocol_details_id: i32, 111 | datap_contract: &contracts::aave_v3::DataProviderContract::DataProviderContractInstance<(), P>, 112 | addressp_address: &Address, 113 | target: String, 114 | ) -> Result<(), Box> { 115 | let secs = 70; 116 | let now = OffsetDateTime::now_utc().unix_timestamp(); 117 | let indices = db_addr 118 | .send(database::GetReservesLiquidityIndices(target)) 119 | .await??; 120 | 121 | let users_guard = users.lock().await; 122 | let total_users = users_guard.len(); 123 | info!("Starting update for {} total users", total_users); 124 | 125 | let mut recent_updates = 0; 126 | for (user, data) in users_guard.iter() { 127 | if now - data.last_update < secs { 128 | let user_positions = 129 | user_positions(datap_contract, addressp_address, user, &indices).await?; 130 | recent_updates += 1; 131 | 132 | info!( 133 | "Updating user {} with HF {} (timestamp: {})", 134 | user, data.health_factor, data.last_update 135 | ); 136 | db_addr 137 | .send(database::UpsertUserData { 138 | address: user.to_string(), 139 | health_factor: data.health_factor, 140 | protocol_details_id, 141 | positions: user_positions, 142 | }) 143 | .await??; 144 | } 145 | } 146 | 147 | info!( 148 | "Updated {}/{} users with recent data (<{}s old)", 149 | recent_updates, total_users, secs 150 | ); 151 | Ok(()) 152 | } 153 | 154 | impl Fanatic

{ 155 | pub async fn new(config: FanaticConfig

) -> eyre::Result> { 156 | let protocol_details_id = config 157 | .db_addr 158 | .send(database::GetProtocolDetailsId(config.target.clone())) 159 | .await??; 160 | let contracts_addr = config 161 | .db_addr 162 | .send(database::GetProtocolContracts(config.target.clone())) 163 | .await??; 164 | 165 | let (pool_contract, datap_contract, addressp_contract) = match ( 166 | contracts_addr.get("Pool"), 167 | contracts_addr.get("UiPoolDataProviderV3"), 168 | contracts_addr.get("PoolAddressesProvider"), 169 | ) { 170 | (Some(pool_addr), Some(datap_addr), Some(provider_addr)) => ( 171 | contracts::aave_v3::PoolContract::new(*pool_addr, config.provider.clone()), 172 | contracts::aave_v3::DataProviderContract::new(*datap_addr, config.provider.clone()), 173 | contracts::aave_v3::AddressProviderContract::new( 174 | *provider_addr, 175 | config.provider.clone(), 176 | ), 177 | ), 178 | _ => return Err(eyre::eyre!("Missing required contract addresses")), 179 | }; 180 | 181 | let (users, prices) = config 182 | .db_addr 183 | .send(database::GetReservesUsers(config.target.clone())) 184 | .await??; 185 | 186 | info!("Reserves Users: {:?}", users); 187 | info!("Reserves Prices: {:#?}", prices); 188 | 189 | Ok(Fanatic { 190 | provider: config.provider, 191 | db_addr: config.db_addr, 192 | follower_addr: config.follower_addr, 193 | executor_addr: None, 194 | pool_contract, 195 | datap_contract, 196 | addressp_contract, 197 | users: Arc::new(Mutex::new(users)), 198 | reserves: Arc::new(Mutex::new(prices)), 199 | target: config.target.clone(), 200 | protocol_details_id, 201 | }) 202 | } 203 | 204 | pub async fn init(self) -> eyre::Result { 205 | self._init_contracts().await?; 206 | self._init_reserves().await?; 207 | 208 | Ok(self) 209 | } 210 | 211 | // TODO 212 | async fn _init_contracts(&self) -> eyre::Result<()> { 213 | Ok(()) 214 | } 215 | 216 | async fn _init_reserves(&self) -> eyre::Result<()> { 217 | let reserves = self 218 | .db_addr 219 | .send(database::GetReserves(self.target.clone())) 220 | .await??; 221 | 222 | if reserves.is_empty() { 223 | info!("No reserves found"); 224 | let reserves = self 225 | .datap_contract 226 | .getReservesData(*self.addressp_contract.address()) 227 | .call() 228 | .await?; 229 | 230 | info!("reserves data: {:#?}", reserves); 231 | let mut init_reserves = Vec::new(); 232 | for reserve in reserves._0.iter() { 233 | let aggregator_addr = self.aggregator_addr(&reserve.priceOracle).await.ok(); 234 | info!("aggregator_addr: {:#?}", aggregator_addr); 235 | 236 | let price = oracle_price(&self.provider, reserve.priceOracle).await; 237 | info!("price: {:?}", price); 238 | 239 | init_reserves.push(database::UpsertReserve { 240 | symbol: reserve.symbol.to_string(), 241 | name: reserve.name.to_string(), 242 | decimals: reserve.decimals.to::() as i32, 243 | address: reserve.underlyingAsset.to_string(), 244 | protocol_details_id: self.protocol_details_id, 245 | liquidation_threshold: norm( 246 | reserve.reserveLiquidationThreshold, 247 | Some(10.0_f64.powf(-2.0)), 248 | )?, 249 | // https://aave.com/docs/developers/smart-contracts/pool-configurator#only-risk-or-pool-admins-methods-configurereserveascollateral 250 | // All the values are expressed in bps. A value of 10000 results in 100.00%. 251 | // The liquidationBonus is always above 100%. 252 | // A value of 105% means the liquidator will receive a 5% bonus. 253 | liquidation_bonus: if (norm(reserve.reserveLiquidationBonus, None)? - 10_000.0) 254 | / 100.0 255 | > 0.0 256 | { 257 | (norm(reserve.reserveLiquidationBonus, None)? - 10_000.0) / 100.0 258 | } else { 259 | 0.0 260 | }, 261 | flashloan_enabled: reserve.flashLoanEnabled, 262 | // You can choose to store the aggregator address or the original oracle address. 263 | // Here we store the original oracle address (as a string) for reference. 264 | oracle_addr: reserve.priceOracle.to_string(), 265 | aggregator_addr: aggregator_addr.map(|v| v.to_string()), 266 | price_usd: price, 267 | stats: database::UpsertReserveStats { 268 | reserve: reserve.underlyingAsset.to_string(), 269 | liquidity_rate: norm(reserve.liquidityRate, Some(100.0 / RAY))?, 270 | variable_borrow_rate: norm(reserve.variableBorrowRate, Some(100.0 / RAY))?, 271 | liquidity_index: norm(reserve.liquidityIndex, Some(1.0 / RAY))?, 272 | variable_borrow_index: norm(reserve.variableBorrowIndex, Some(1.0 / RAY))?, 273 | }, 274 | }); 275 | } 276 | 277 | let (network, _) = self.target.split_once('-').unwrap(); 278 | self.db_addr 279 | .send(database::UpsertReserves { 280 | network_id: network.to_string(), 281 | reserves: init_reserves, 282 | }) 283 | .await??; 284 | } 285 | 286 | Ok(()) 287 | } 288 | 289 | pub async fn aggregator_addr(&self, oracle_addr: &Address) -> eyre::Result

{ 290 | // 1. Attempt using the CLRatePriceCapAdapter first. 291 | let adapter = 292 | contracts::chainlink::CLRatePriceCapAdapterContract::new(*oracle_addr, &self.provider); 293 | if let Ok(resp) = adapter.BASE_TO_USD_AGGREGATOR().call().await { 294 | return Ok(resp._0); 295 | } 296 | 297 | // 2. Next, try using CLSynchronicityPriceAdapterPegToBase. 298 | let synch_adapter = contracts::chainlink::CLSynchronicityPriceAdapterPegToBaseContract::new( 299 | *oracle_addr, 300 | &self.provider, 301 | ); 302 | if let Ok(resp) = synch_adapter.ASSET_TO_PEG().call().await { 303 | // Use the returned address to initialize EACAggregatorProxy and call aggregator(). 304 | let proxy = 305 | contracts::chainlink::EACAggregatorProxyContract::new(resp._0, &self.provider); 306 | if let Ok(proxy_resp) = proxy.aggregator().call().await { 307 | return Ok(proxy_resp._0); 308 | } 309 | } 310 | 311 | // 3. Next, try using EACAggregatorProxy directly on the original oracle address. 312 | let proxy = 313 | contracts::chainlink::EACAggregatorProxyContract::new(*oracle_addr, &self.provider); 314 | if let Ok(res) = proxy.aggregator().call().await { 315 | return Ok(res._0); 316 | } 317 | 318 | // 4. As fallback, try PriceCapAdapterStable's ASSET_TO_USD_AGGREGATOR. 319 | let stable_adapter = 320 | contracts::chainlink::PriceCapAdapterStableContract::new(*oracle_addr, &self.provider); 321 | if let Ok(resp_stable) = stable_adapter.ASSET_TO_USD_AGGREGATOR().call().await { 322 | let new_proxy = contracts::chainlink::EACAggregatorProxyContract::new( 323 | resp_stable._0, 324 | &self.provider, 325 | ); 326 | let res_new = new_proxy.aggregator().call().await?; 327 | return Ok(res_new._0); 328 | } 329 | 330 | warn!( 331 | "couldn't acquire aggregator address for reserve with oracle {}", 332 | oracle_addr 333 | ); 334 | Err(eyre::eyre!("fetch_base_to_usd_addr: all calls failed")) 335 | } 336 | } 337 | 338 | impl Handler for Fanatic

{ 339 | type Result = ResponseFuture<()>; 340 | 341 | fn handle(&mut self, msg: UpdateReservePrice, _ctx: &mut Self::Context) -> Self::Result { 342 | // TODO: we have to recalculate the new users' health factors 343 | // we'll initially do a brute force calculation, i.e calculate the new health factors for all users 344 | let executor_addr = self.executor_addr.clone().unwrap(); 345 | let reserve_addr = msg.reserve; 346 | let new_price = msg.new_price; 347 | 348 | let target = self.target.clone(); 349 | let pool_contract = self.pool_contract.clone(); 350 | 351 | let reserves = self.reserves.clone(); 352 | let users = self.users.clone(); 353 | 354 | let fut = async move { 355 | // update the reserve price 356 | { 357 | let mut reserves = reserves.lock().await; 358 | if let Some(reserve_data) = reserves.get_mut(&reserve_addr) { 359 | info!( 360 | ?reserve_addr, 361 | old_price = reserve_data.price, 362 | new_price, 363 | "update_reserve_price" 364 | ); 365 | reserve_data.price = new_price; 366 | } else { 367 | warn!(?reserve_addr, "Unable to find reserve price"); 368 | } 369 | } 370 | 371 | // TODO: tons of possibilities for improvements. this won't do that well in the long run 372 | // when we manage tons and tons of users. 373 | let reserves = reserves.lock().await; 374 | let reserve = reserves.get(&reserve_addr); 375 | if let Some(reserve_data) = reserve { 376 | let users_lock = users.lock().await; 377 | let high_prio = reserve_data 378 | .users 379 | .iter() 380 | .filter(|user| { 381 | users_lock.get(*user).is_some() 382 | && users_lock.get(*user).unwrap().health_factor < 1.05 383 | }) 384 | .collect::>(); 385 | 386 | for user in high_prio { 387 | let hf = match health_factor(&pool_contract, *user).await { 388 | Some(hf) => hf, 389 | None => return, 390 | }; 391 | 392 | if hf < 1.0 { 393 | let (network, protocol) = target.split_once('-').unwrap(); 394 | let (network, protocol) = (network.to_string(), protocol.to_string()); 395 | 396 | let payload = LiquidationRequest { 397 | user_address: *user, 398 | network: network, 399 | protocol: protocol, 400 | }; 401 | 402 | let result = executor_addr.send(payload).await; 403 | match result { 404 | Ok(_) => info!("sent liquidation request for user {}", user), 405 | Err(e) => error!( 406 | "failed to send liquidation request for user {}: {}", 407 | user, e 408 | ), 409 | } 410 | 411 | let mut users = users.lock().await; 412 | users.insert( 413 | *user, 414 | database::UserData { 415 | health_factor: hf, 416 | last_update: OffsetDateTime::now_utc().unix_timestamp(), 417 | }, 418 | ); 419 | } 420 | } 421 | 422 | for user in &reserve_data.users { 423 | let hf = match health_factor(&pool_contract, *user).await { 424 | Some(hf) => hf, 425 | None => return, 426 | }; 427 | 428 | if hf < 1.0 { 429 | let (network, protocol) = target.split_once('-').unwrap(); 430 | let (network, protocol) = (network.to_string(), protocol.to_string()); 431 | 432 | let payload = LiquidationRequest { 433 | user_address: *user, 434 | network: network, 435 | protocol: protocol, 436 | }; 437 | 438 | let result = executor_addr.send(payload).await; 439 | match result { 440 | Ok(_) => info!("sent liquidation request for user {}", user), 441 | Err(e) => error!( 442 | "failed to send liquidation request for user {}: {}", 443 | user, e 444 | ), 445 | } 446 | } 447 | 448 | if hf < 100.0 { 449 | let mut users = users.lock().await; 450 | users.insert( 451 | *user, 452 | database::UserData { 453 | health_factor: hf, 454 | last_update: OffsetDateTime::now_utc().unix_timestamp(), 455 | }, 456 | ); 457 | } 458 | } 459 | } else { 460 | info!("No users found in reserve"); 461 | } 462 | }; 463 | 464 | Box::pin(fut) 465 | } 466 | } 467 | 468 | impl Handler for Fanatic

{ 469 | type Result = ResponseFuture<()>; 470 | 471 | fn handle(&mut self, msg: UpdateReserveUser, _ctx: &mut Self::Context) -> Self::Result { 472 | let reserve_addr = msg.reserve; 473 | let user = msg.user_addr; 474 | let executor_addr = self.executor_addr.clone().unwrap(); 475 | 476 | let target = self.target.clone(); 477 | 478 | let pool_contract = self.pool_contract.clone(); 479 | 480 | let reserves = self.reserves.clone(); 481 | let users = self.users.clone(); 482 | 483 | let fut = async move { 484 | let hf = match health_factor(&pool_contract, user).await { 485 | Some(hf) => hf, 486 | None => return, 487 | }; 488 | 489 | { 490 | let users = users.lock().await; 491 | let user_data = users 492 | .get(&user) 493 | .cloned() 494 | .unwrap_or(database::UserData::default()); 495 | info!( 496 | "user={} | last updated at {} | health factor changed from {} to {}", 497 | user, user_data.last_update, user_data.health_factor, hf 498 | ); 499 | } 500 | 501 | if hf < 1.0 { 502 | let (network, protocol) = target.split_once('-').unwrap(); 503 | let (network, protocol) = (network.to_string(), protocol.to_string()); 504 | 505 | let payload = LiquidationRequest { 506 | user_address: user, 507 | network: network, 508 | protocol: protocol, 509 | }; 510 | 511 | let result = executor_addr.send(payload).await; 512 | match result { 513 | Ok(_) => info!("sent liquidation request for user {} with HF {}", user, hf), 514 | Err(e) => error!( 515 | "failed to send liquidation request for user {}: {}", 516 | user, e 517 | ), 518 | } 519 | } 520 | 521 | // sanity check 522 | if hf < 100.0 { 523 | let mut reserves = reserves.lock().await; 524 | if let Some(reserve) = reserves.get_mut(&reserve_addr) { 525 | reserve.users.insert(user); 526 | } 527 | 528 | let mut users = users.lock().await; 529 | users.insert( 530 | user, 531 | database::UserData { 532 | health_factor: hf, 533 | last_update: OffsetDateTime::now_utc().unix_timestamp(), 534 | }, 535 | ); 536 | } 537 | }; 538 | 539 | Box::pin(fut) 540 | } 541 | } 542 | 543 | impl Handler for Fanatic

{ 544 | type Result = ResponseFuture>; 545 | 546 | fn handle(&mut self, msg: DoSmthWithLiquidationCall, _: &mut Self::Context) -> Self::Result { 547 | let protocol_details_id = self.protocol_details_id; 548 | let db_addr = self.db_addr.clone(); 549 | 550 | let fut = async move { 551 | db_addr 552 | .send(database::InsertLiquidationCall { 553 | call: msg.0, 554 | protocol_details_id, 555 | }) 556 | .await??; 557 | 558 | Ok(()) 559 | }; 560 | 561 | Box::pin(fut) 562 | } 563 | } 564 | 565 | impl Handler for Fanatic

{ 566 | type Result = ResponseFuture>; 567 | 568 | fn handle(&mut self, _: Shutdown, _: &mut Self::Context) -> Self::Result { 569 | let users = self.users.clone(); 570 | let db_addr = self.db_addr.clone(); 571 | let datap_contract = self.datap_contract.clone(); 572 | let addressp_address = *self.addressp_contract.address(); 573 | let protocol_details_id = self.protocol_details_id; 574 | let target = self.target.clone(); 575 | 576 | let fut = async move { 577 | if let Err(e) = update_recent_users( 578 | &users, 579 | &db_addr, 580 | protocol_details_id, 581 | &datap_contract, 582 | &addressp_address, 583 | target, 584 | ) 585 | .await 586 | { 587 | error!("Failed to update recent users: {}", e); 588 | } else { 589 | info!("[shutdown] upsert all users positions & health factors..done"); 590 | } 591 | 592 | Ok(()) 593 | }; 594 | 595 | Box::pin(fut) 596 | } 597 | } 598 | 599 | impl Handler for Fanatic

{ 600 | type Result = ResponseActFuture>; 601 | 602 | fn handle(&mut self, msg: SuccessfulLiquidation, _: &mut Context) -> Self::Result { 603 | let users = self.users.clone(); 604 | 605 | Box::pin( 606 | async move { 607 | let mut users_guard = users.lock().await; 608 | if let Some(user_data) = users_guard.get_mut(&msg.user_addr) { 609 | user_data.health_factor = -2.0; // Liquidated 610 | user_data.last_update = OffsetDateTime::now_utc().unix_timestamp(); 611 | } 612 | Ok(()) 613 | } 614 | .into_actor(self), 615 | ) 616 | } 617 | } 618 | 619 | impl Handler for Fanatic

{ 620 | type Result = ResponseActFuture>; 621 | 622 | fn handle(&mut self, msg: FailedLiquidation, _: &mut Context) -> Self::Result { 623 | let users = self.users.clone(); 624 | 625 | Box::pin( 626 | async move { 627 | let mut users_guard = users.lock().await; 628 | if let Some(user_data) = users_guard.get_mut(&msg.user_addr) { 629 | user_data.last_update = OffsetDateTime::now_utc().unix_timestamp(); 630 | } 631 | Ok(()) 632 | } 633 | .into_actor(self), 634 | ) 635 | } 636 | } 637 | --------------------------------------------------------------------------------