├── onchain ├── test │ └── .gitkeep ├── .gitignore ├── Makefile ├── migrations │ └── 1_initial_migration.js ├── package.json ├── contracts │ ├── Migrations.sol │ └── Snipe.sol └── truffle-config.js └── watcher ├── config.json ├── .gitignore ├── src ├── lib.rs ├── main.rs ├── config.rs ├── update.rs └── compound.rs ├── Makefile └── Cargo.toml /onchain/test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcher/config.json: -------------------------------------------------------------------------------- 1 | { "addr": "zbc" } 2 | -------------------------------------------------------------------------------- /watcher/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target 3 | -------------------------------------------------------------------------------- /onchain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /package-lock.json 4 | -------------------------------------------------------------------------------- /watcher/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod compound; 2 | pub mod config; 3 | pub mod update; 4 | 5 | use once_cell::sync::Lazy; 6 | pub static CONFIG: Lazy = Lazy::new(|| config::load()); 7 | -------------------------------------------------------------------------------- /onchain/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | all: 4 | ./node_modules/.bin/truffle build 5 | 6 | deploy: 7 | ./node_modules/.bin/truffle deploy 8 | 9 | format: 10 | npx prettier --write contracts/**/*.sol 11 | 12 | -------------------------------------------------------------------------------- /onchain/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | const Snipe = artifacts.require("Snipe"); 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(Migrations); 6 | deployer.deploy(Snipe); 7 | }; 8 | -------------------------------------------------------------------------------- /watcher/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test 2 | 3 | all: 4 | cargo build 5 | 6 | run: all 7 | PWD=`pwd` ./target/debug/`basename ${PWD}` 8 | 9 | format: 10 | find src -type f -exec rustfmt --edition 2021 {} \; 11 | 12 | test: 13 | cargo test -- --nocapture 14 | -------------------------------------------------------------------------------- /onchain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@uniswap/v3-periphery": "^1.4.1", 4 | "prettier": "^2.7.1", 5 | "prettier-plugin-solidity": "^1.0.0-beta.19", 6 | "truffle": "*" 7 | }, 8 | "dependencies": { 9 | "ganache": "^7.3.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /watcher/src/main.rs: -------------------------------------------------------------------------------- 1 | use redis; 2 | use watcher::*; 3 | 4 | fn main() { 5 | let redis = redis::Client::open("redis://127.0.0.1/").unwrap(); 6 | println!("compound monitor"); 7 | match update::update() { 8 | Ok(accounts) => update::merge(redis, accounts), 9 | Err(e) => println!("{}", e), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /watcher/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json; 3 | use std::fs; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct Config { 7 | pub addr: String, 8 | } 9 | 10 | pub fn load() -> Config { 11 | let reader = fs::File::open("config.json").unwrap(); 12 | serde_json::from_reader(reader).unwrap() 13 | } 14 | -------------------------------------------------------------------------------- /watcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "watcher" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest= { version = "0.11", features = ["blocking"] } 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | redis = "0.21" 13 | once_cell = "1.12" 14 | 15 | 16 | -------------------------------------------------------------------------------- /onchain/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint256 public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint256 completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /watcher/src/update.rs: -------------------------------------------------------------------------------- 1 | use crate::compound; 2 | use redis; 3 | use redis::Commands; 4 | use reqwest; 5 | 6 | pub fn update() -> Result { 7 | use reqwest::blocking::Client; 8 | let client = Client::new(); 9 | let url = reqwest::Url::parse(compound::ACCOUNTS_URL).unwrap(); 10 | println!("{}", url); 11 | let accounts_options = compound::AccountOptions { 12 | page_size: 15, 13 | max_health: compound::Value { 14 | value: "1.02".to_string(), 15 | }, 16 | }; 17 | let options_json = serde_json::to_string(&accounts_options).unwrap(); 18 | let resp = client.post(url).body(options_json).send()?; 19 | 20 | let accounts: compound::Response = serde_json::from_str(&resp.text()?).unwrap(); 21 | 22 | Ok(accounts) 23 | } 24 | 25 | pub fn merge(redis: redis::Client, response: compound::Response) { 26 | let mut r = redis.get_connection().unwrap(); 27 | let _: Result = r.hset("accounts", "a", "zoo"); 28 | println!("{}", response.accounts.len()); 29 | for account in response.accounts { 30 | println!("{}", account); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /onchain/contracts/Snipe.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; 5 | 6 | contract Snipe { 7 | ISwapRouter public immutable swapRouter; 8 | 9 | address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; 10 | address private constant CETH = 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5; 11 | 12 | constructor(ISwapRouter _swapRouter) { 13 | swapRouter = _swapRouter; 14 | } 15 | 16 | function liquidate( 17 | address _borrower, 18 | address _repayCToken, 19 | address _seizeCToken, 20 | uint256 _repay, 21 | uint256 _seize 22 | ) private returns (uint256) { 23 | // For this example, we will set the pool fee to 0.3%. 24 | uint24 poolFee = 3000; 25 | 26 | ISwapRouter.ExactInputSingleParams memory params = ISwapRouter 27 | .ExactInputSingleParams({ 28 | tokenIn: DAI, 29 | tokenOut: _repayCToken, 30 | fee: poolFee, 31 | recipient: msg.sender, 32 | deadline: block.timestamp, 33 | amountIn: _repay, 34 | amountOutMinimum: 0, 35 | sqrtPriceLimitX96: 0 36 | }); 37 | 38 | swapRouter.exactInputSingle(params); 39 | 40 | return _repay + _seize; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /watcher/src/compound.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use std::fmt; 4 | 5 | pub static ACCOUNTS_URL: &str = "https://api.compound.finance/api/v2/account"; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | pub struct Response { 9 | pub accounts: Vec, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub struct AccountOptions { 14 | pub max_health: Value, 15 | pub page_size: i32, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct Account { 20 | pub address: String, 21 | pub health: Option, 22 | pub total_borrow_value_in_eth: Option, 23 | pub total_collateral_value_in_eth: Option, 24 | pub tokens: Vec, 25 | } 26 | 27 | #[derive(Serialize, Deserialize)] 28 | pub struct Value { 29 | pub value: String, 30 | } 31 | 32 | impl Value { 33 | fn value(&self) -> f64 { 34 | self.value.parse::().unwrap() 35 | } 36 | } 37 | 38 | #[derive(Serialize, Deserialize)] 39 | pub struct Token { 40 | pub symbol: String, 41 | pub borrow_balance_underlying: Value, 42 | pub supply_balance_underlying: Value, 43 | } 44 | 45 | impl fmt::Display for Account { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | let mut parts: Vec = vec![]; 48 | 49 | parts.push(self.address.to_owned()); 50 | match &self.health { 51 | Some(h) => parts.push(h.value[..6].to_owned()), 52 | None => (), 53 | } 54 | match &self.total_borrow_value_in_eth { 55 | Some(borrow) => { 56 | let m = format!("bow: {}", borrow.value[..6].to_string()); 57 | parts.push(m) 58 | } 59 | None => (), 60 | } 61 | match &self.total_collateral_value_in_eth { 62 | Some(collateral) => { 63 | let m = format!("col: {}", collateral.value[..6].to_string()); 64 | parts.push(m) 65 | } 66 | None => (), 67 | } 68 | 69 | let mut map: HashMap<&str, (f64, f64)> = HashMap::new(); 70 | 71 | for token in &self.tokens { 72 | let supply = token.supply_balance_underlying.value(); 73 | let borrow = token.borrow_balance_underlying.value(); 74 | map.insert(&token.symbol, (supply, borrow)); 75 | } 76 | 77 | let dollarcoins = vec!["cUSDT", "cDAI", "cUSDC"]; 78 | for token in &self.tokens { 79 | let supply = token.supply_balance_underlying.value(); 80 | let borrow = token.borrow_balance_underlying.value(); 81 | if dollarcoins.contains(&token.symbol.as_str()) { 82 | match map.get("cETH") { 83 | Some(c_eth) => { 84 | let eth_supply = c_eth.0; 85 | if eth_supply > 0.0 && borrow > 0.0001 { 86 | let liq_price = borrow / eth_supply; 87 | let m = format!( 88 | "{}:{:.4} ${:.4}", 89 | token.symbol.to_owned(), 90 | supply, 91 | liq_price, 92 | ); 93 | parts.push(m) 94 | } 95 | } 96 | None => (), 97 | } 98 | } 99 | } 100 | write!(f, "{}", parts.join(" ")) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /onchain/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation, and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * https://trufflesuite.com/docs/truffle/reference/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // 23 | // const fs = require('fs'); 24 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 25 | 26 | module.exports = { 27 | /** 28 | * Networks define how you connect to your ethereum client and let you set the 29 | * defaults web3 uses to send transactions. If you don't specify one truffle 30 | * will spin up a development blockchain for you on port 9545 when you 31 | * run `develop` or `test`. You can ask a truffle command to use a specific 32 | * network from the command line, e.g 33 | * 34 | * $ truffle test --network 35 | */ 36 | 37 | networks: { 38 | // Useful for testing. The `development` name is special - truffle uses it by default 39 | // if it's defined here and no other network is specified at the command line. 40 | // You should run a client (like ganache, geth, or parity) in a separate terminal 41 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 42 | // options below to some value. 43 | // 44 | // development: { 45 | // host: "127.0.0.1", // Localhost (default: none) 46 | // port: 8545, // Standard Ethereum port (default: none) 47 | // network_id: "*", // Any network (default: none) 48 | // }, 49 | // 50 | // An additional network, but with some advanced options… 51 | // advanced: { 52 | // port: 8777, // Custom port 53 | // network_id: 1342, // Custom network 54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 56 | // from:
, // Account to send transactions from (default: accounts[0]) 57 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 58 | // }, 59 | // 60 | // Useful for deploying to a public network. 61 | // Note: It's important to wrap the provider as a function to ensure truffle uses a new provider every time. 62 | // ropsten: { 63 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 64 | // network_id: 3, // Ropsten's id 65 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 66 | // confirmations: 2, // # of confirmations to wait between deployments. (default: 0) 67 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 68 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 69 | // }, 70 | // 71 | // Useful for private networks 72 | // private: { 73 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 74 | // network_id: 2111, // This network is yours, in the cloud. 75 | // production: true // Treats this network as if it was a public net. (default: false) 76 | // } 77 | }, 78 | 79 | // Set default mocha options here, use special reporters, etc. 80 | mocha: { 81 | // timeout: 100000 82 | }, 83 | 84 | // Configure your compilers 85 | compilers: { 86 | solc: { 87 | version: "0.8.14", // Fetch exact version from solc-bin (default: truffle's version) 88 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 89 | // settings: { // See the solidity docs for advice about optimization and evmVersion 90 | // optimizer: { 91 | // enabled: false, 92 | // runs: 200 93 | // }, 94 | // evmVersion: "byzantium" 95 | // } 96 | } 97 | }, 98 | 99 | // Truffle DB is currently disabled by default; to enable it, change enabled: 100 | // false to enabled: true. The default storage location can also be 101 | // overridden by specifying the adapter settings, as shown in the commented code below. 102 | // 103 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 104 | // make a backup of your artifacts to a safe location before enabling this feature. 105 | // 106 | // After you backed up your artifacts you can utilize db by running migrate as follows: 107 | // $ truffle migrate --reset --compile-all 108 | // 109 | // db: { 110 | // enabled: false, 111 | // host: "127.0.0.1", 112 | // adapter: { 113 | // name: "sqlite", 114 | // settings: { 115 | // directory: ".db" 116 | // } 117 | // } 118 | // } 119 | }; 120 | --------------------------------------------------------------------------------