Incomplete prototype
Things WIP Format Work in progress Work in progres Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress Work in progress
This commit is contained in:
commit
7a1e03ee7a
19 changed files with 7242 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
33
.github/workflows/build.yaml
vendored
Normal file
33
.github/workflows/build.yaml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: Nix Flake actions
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nix-matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: cachix/install-nix-action@v30
|
||||||
|
- id: set-matrix
|
||||||
|
name: Generate Nix Matrix
|
||||||
|
run: |
|
||||||
|
set -Eeu
|
||||||
|
matrix="$(nix eval --json '.#githubActions.matrix')"
|
||||||
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
nix-build:
|
||||||
|
name: ${{ matrix.name }} (${{ matrix.system }})
|
||||||
|
needs: nix-matrix
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix: ${{fromJSON(needs.nix-matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: cachix/install-nix-action@v30
|
||||||
|
- run: nix build -L '.#${{ matrix.attr }}'
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
offchain/target
|
||||||
|
offchain/config.kdl
|
||||||
|
offchain/pairs.json
|
||||||
|
onchain/lib
|
||||||
|
onchain/out
|
||||||
|
onchain/cache
|
||||||
|
.direnv
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
**/result
|
197
flake.lock
generated
Normal file
197
flake.lock
generated
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1741352980,
|
||||||
|
"narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-root": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1723604017,
|
||||||
|
"narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=",
|
||||||
|
"owner": "srid",
|
||||||
|
"repo": "flake-root",
|
||||||
|
"rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "srid",
|
||||||
|
"repo": "flake-root",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forge-std": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1738330218,
|
||||||
|
"narHash": "sha256-4y1Hf0Te2oJxwKBOgVBEHZeKYt7hs+wTgdIO+rItj0E=",
|
||||||
|
"owner": "foundry-rs",
|
||||||
|
"repo": "forge-std",
|
||||||
|
"rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "foundry-rs",
|
||||||
|
"ref": "v1.9.6",
|
||||||
|
"repo": "forge-std",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1742649964,
|
||||||
|
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-github-actions": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1737420293,
|
||||||
|
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nix-github-actions",
|
||||||
|
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nix-github-actions",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1742669843,
|
||||||
|
"narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "1e5b653dff12029333a6546c11e108ede13052eb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1740877520,
|
||||||
|
"narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "147dee35aab2193b174e4c0868bd80ead5ce755c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"flake-root": "flake-root",
|
||||||
|
"forge-std": "forge-std",
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"nix-github-actions": "nix-github-actions",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1742982148,
|
||||||
|
"narHash": "sha256-aRA6LSxjlbMI6MmMzi/M5WH/ynd8pK+vACD9za3MKLQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "61c88349bf6dff49fa52d7dfc39b21026c2a8881",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
115
flake.nix
Normal file
115
flake.nix
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
treefmt-nix = {
|
||||||
|
url = "github:numtide/treefmt-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
git-hooks = {
|
||||||
|
url = "github:cachix/git-hooks.nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
flake-root.url = "github:srid/flake-root";
|
||||||
|
nix-github-actions = {
|
||||||
|
url = "github:nix-community/nix-github-actions";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
forge-std = {
|
||||||
|
flake = false;
|
||||||
|
url = "github:foundry-rs/forge-std/v1.9.6";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs:
|
||||||
|
inputs.flake-parts.lib.mkFlake { inherit inputs; } ({ config, lib, ... }: {
|
||||||
|
systems = [ "x86_64-linux" ];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
inputs.git-hooks.flakeModule
|
||||||
|
inputs.treefmt-nix.flakeModule
|
||||||
|
inputs.flake-root.flakeModule
|
||||||
|
];
|
||||||
|
|
||||||
|
perSystem = { pkgs, config, ... }: {
|
||||||
|
treefmt.config = {
|
||||||
|
flakeFormatter = true;
|
||||||
|
flakeCheck = true;
|
||||||
|
programs = {
|
||||||
|
nixpkgs-fmt.enable = true;
|
||||||
|
rustfmt.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pre-commit = {
|
||||||
|
check.enable = false;
|
||||||
|
settings.hooks = {
|
||||||
|
treefmt = {
|
||||||
|
enable = true;
|
||||||
|
package = config.treefmt.build.wrapper;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [ cargo rustc rust-analyzer clippy foundry ];
|
||||||
|
inputsFrom = [ config.flake-root.devShell ];
|
||||||
|
shellHook = ''
|
||||||
|
# forge will use this directory to download the solc compilers
|
||||||
|
mkdir -p $HOME/.svm
|
||||||
|
|
||||||
|
# forge needs forge-std to work
|
||||||
|
mkdir -p $FLAKE_ROOT/onchain/lib/
|
||||||
|
ln -sf ${inputs.forge-std.outPath} $FLAKE_ROOT/onchain/lib/forge-std
|
||||||
|
|
||||||
|
if [ ! -f "$FLAKE_ROOT/offchain/config.kdl" ]; then \
|
||||||
|
cp ${config.packages.arbi_sample_config_kdl} $FLAKE_ROOT/offchain/config.kdl
|
||||||
|
fi
|
||||||
|
export ARBI_CONFIG="$FLAKE_ROOT/offchain/config.kdl"
|
||||||
|
|
||||||
|
${config.pre-commit.installationScript}
|
||||||
|
'';
|
||||||
|
env = {
|
||||||
|
OPENSSL_DIR = pkgs.openssl.dev;
|
||||||
|
OPENSSL_NO_VENDOR = true;
|
||||||
|
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
|
||||||
|
OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
|
||||||
|
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH";
|
||||||
|
RUST_BACKTRACE = true;
|
||||||
|
ARBI_LOG_LEVEL = "debug";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
default = config.packages.arbi;
|
||||||
|
|
||||||
|
arbi = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "arbi";
|
||||||
|
version = "0.1.0";
|
||||||
|
cargoLock.lockFile = ./offchain/Cargo.lock;
|
||||||
|
src = ./offchain;
|
||||||
|
env = {
|
||||||
|
OPENSSL_DIR = pkgs.openssl.dev;
|
||||||
|
OPENSSL_NO_VENDOR = true;
|
||||||
|
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
|
||||||
|
OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
|
||||||
|
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
arbi_sample_config_kdl = pkgs.writeText "arbi-sample-config.kdl" ''
|
||||||
|
endpoint "wss://eth-mainnet.g.alchemy.com/v2/<REDACTED>"
|
||||||
|
pairs_file "pairs.json"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
inherit (config.packages) arbi;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
flake.githubActions = inputs.nix-github-actions.lib.mkGithubMatrix {
|
||||||
|
checks = lib.getAttrs [ "x86_64-linux" ] config.flake.checks;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
4656
offchain/Cargo.lock
generated
Normal file
4656
offchain/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
offchain/Cargo.toml
Normal file
17
offchain/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "arbi"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alloy = { version = "0.12.6", features = ["full"] }
|
||||||
|
clap = { version = "4.5.32", features = ["derive", "env"] }
|
||||||
|
env_logger = "0.11.7"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
kdl = "6.3.4"
|
||||||
|
log = "0.4.27"
|
||||||
|
miette = { version = "7.5.0", features = ["fancy"] }
|
||||||
|
serde = "1.0.219"
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
tokio = { version = "1.44.1", features = ["full"] }
|
1430
offchain/abi/IUniswapV2Pair.json
Normal file
1430
offchain/abi/IUniswapV2Pair.json
Normal file
File diff suppressed because it is too large
Load diff
51
offchain/src/config.rs
Normal file
51
offchain/src/config.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use kdl::KdlDocument;
|
||||||
|
use miette::{miette, Result};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub endpoint: String,
|
||||||
|
pub pairs_file: PathBuf,
|
||||||
|
pub concurrency: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn parse_config(config_file: PathBuf) -> Result<Config> {
|
||||||
|
let config_str = std::fs::read_to_string(config_file).map_err(|e| miette!(e))?;
|
||||||
|
|
||||||
|
let doc = config_str.parse::<KdlDocument>()?;
|
||||||
|
|
||||||
|
let endpoint = doc
|
||||||
|
.get("endpoint")
|
||||||
|
.ok_or(miette!("Missing 'endpoint'"))?
|
||||||
|
.get(0)
|
||||||
|
.ok_or(miette!("'endpoint' has no value"))?
|
||||||
|
.as_string()
|
||||||
|
.ok_or(miette!("'endpoint' value must be a string"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let pairs_file = PathBuf::from(
|
||||||
|
doc.get("pairs_file")
|
||||||
|
.ok_or(miette!("Missing 'pairs_file'"))?
|
||||||
|
.get(0)
|
||||||
|
.ok_or(miette!("'pairs_file' has no value"))?
|
||||||
|
.as_string()
|
||||||
|
.ok_or(miette!("'pairs_file' value must be a string"))?
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let concurrency =
|
||||||
|
doc.get("concurrency")
|
||||||
|
.ok_or(miette!("Missing 'concurrency'"))?
|
||||||
|
.get(0)
|
||||||
|
.ok_or(miette!("'concurrency' has no value"))?
|
||||||
|
.as_integer()
|
||||||
|
.ok_or(miette!("'concurrency' value must be an integer"))? as usize;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
endpoint,
|
||||||
|
pairs_file,
|
||||||
|
concurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
65
offchain/src/main.rs
Normal file
65
offchain/src/main.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod run;
|
||||||
|
|
||||||
|
/// Arbitrage bot
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, author, about)]
|
||||||
|
struct Args {
|
||||||
|
/// Configuration file
|
||||||
|
#[arg(short = 'c', long = "config", env = "ARBI_CONFIG")]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Log level
|
||||||
|
#[arg(long = "log-level", env = "ARBI_LOG_LEVEL", default_value = "info")]
|
||||||
|
log_level: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
enum Command {
|
||||||
|
// Run the arbitrage bot
|
||||||
|
Run {},
|
||||||
|
|
||||||
|
// Check the configuration file
|
||||||
|
CheckConfig {},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> miette::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let log_level_filter = match args.log_level.to_lowercase().as_str() {
|
||||||
|
"debug" => LevelFilter::Debug,
|
||||||
|
"trace" => LevelFilter::Trace,
|
||||||
|
"warn" => LevelFilter::Warn,
|
||||||
|
"error" => LevelFilter::Error,
|
||||||
|
"info" => LevelFilter::Info,
|
||||||
|
_ => LevelFilter::Info, // Default to info
|
||||||
|
};
|
||||||
|
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.filter_level(log_level_filter)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Command::Run {} => {
|
||||||
|
let config = config::Config::parse_config(args.config)?;
|
||||||
|
|
||||||
|
run::run(config)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Command::CheckConfig {} => {
|
||||||
|
let config = config::Config::parse_config(args.config)?;
|
||||||
|
|
||||||
|
println!("Configuration correctly parsed\n{:#?}", config);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
offchain/src/pairs.rs
Normal file
166
offchain/src/pairs.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
use alloy::primitives::{aliases::U112, Address};
|
||||||
|
|
||||||
|
use miette::{miette, Result};
|
||||||
|
use serde::de::{self, Visitor};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Pair {
|
||||||
|
token0: Address,
|
||||||
|
token1: Address,
|
||||||
|
reserve0: U112,
|
||||||
|
reserve1: U112,
|
||||||
|
factory: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||||
|
struct AddressPair(Address, Address);
|
||||||
|
|
||||||
|
impl Serialize for AddressPair {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(format!("{:?}:{:?}", self.0, self.1).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for AddressPair {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct AddressPairVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for AddressPairVisitor {
|
||||||
|
type Value = AddressPair;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a string containing two addresses separated by a colon (e.g. \"0x...0:0x...0\")")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
let parts: Vec<&str> = value.splitn(2, ':').map(str::trim).collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(E::custom(format!(
|
||||||
|
"Invalid format, expected 'address1:address2' but got \"{}\"",
|
||||||
|
value
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token0 = Address::from_str(parts[0])
|
||||||
|
.map_err(|e| E::custom(format!("Error parsing the address \"{}\"", e)))?;
|
||||||
|
let token1 = Address::from_str(parts[1])
|
||||||
|
.map_err(|e| E::custom(format!("Error parsing the address \"{}\"", e)))?;
|
||||||
|
|
||||||
|
Ok(AddressPair(token0, token1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(AddressPairVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Pairs {
|
||||||
|
pairs: HashMap<Address, Pair>,
|
||||||
|
by_tokens: HashMap<AddressPair, Vec<Address>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pairs {
|
||||||
|
pub fn new(filename: &PathBuf) -> Result<Self> {
|
||||||
|
debug!("Looking for pairs file in {:?}", filename);
|
||||||
|
|
||||||
|
let path = std::path::Path::new(&filename);
|
||||||
|
if path.exists() {
|
||||||
|
info!("Found existing {:?}", filename);
|
||||||
|
|
||||||
|
let data = std::fs::read_to_string(path).map_err(|e| miette!(e))?;
|
||||||
|
serde_json::from_str(&data).map_err(|e| miette!(e))
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"File {:?} doesn't exist, creating new structure from scratch",
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Pairs {
|
||||||
|
pairs: HashMap::new(),
|
||||||
|
by_tokens: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, address: Address) -> Option<&Pair> {
|
||||||
|
self.pairs.get(&address)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(
|
||||||
|
&mut self,
|
||||||
|
pair: Address,
|
||||||
|
token0: Address,
|
||||||
|
token1: Address,
|
||||||
|
reserve0: U112,
|
||||||
|
reserve1: U112,
|
||||||
|
factory: Address,
|
||||||
|
) -> Option<()> {
|
||||||
|
let old = self.pairs.insert(
|
||||||
|
pair,
|
||||||
|
Pair {
|
||||||
|
token0,
|
||||||
|
token1,
|
||||||
|
reserve0,
|
||||||
|
reserve1,
|
||||||
|
factory,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match old {
|
||||||
|
Some(old) => {
|
||||||
|
info!("Pair {} already present, updating reserves (reserve0: {} -> {}; reserve1: {} -> {})",
|
||||||
|
pair, old.reserve0, reserve0, old.reserve1, reserve1
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("First time seeing pair {}, adding it", { pair });
|
||||||
|
|
||||||
|
match self.by_tokens.get_mut(&AddressPair(token0, token1)) {
|
||||||
|
Some(tokens) => {
|
||||||
|
tokens.push(pair);
|
||||||
|
info!(
|
||||||
|
"Already know {} pairs with tokens {:?} and {:?}",
|
||||||
|
tokens.len(),
|
||||||
|
token0,
|
||||||
|
token1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.by_tokens
|
||||||
|
.insert(AddressPair(token0, token1), vec![pair]);
|
||||||
|
info!(
|
||||||
|
"It's the first pair with tokens {:?} and {:?}",
|
||||||
|
token0, token1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump(&self, filename: &PathBuf) -> Result<()> {
|
||||||
|
let data = serde_json::to_string(&self).map_err(|e| miette!(e))?;
|
||||||
|
std::fs::write(filename, data).map_err(|e| miette!(e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
44
offchain/src/priority_queue.rs
Normal file
44
offchain/src/priority_queue.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
mod pairs;
|
||||||
|
|
||||||
|
use alloy::primitives::Address;
|
||||||
|
use futures_util::Stream;
|
||||||
|
use log::debug;
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Action {
|
||||||
|
ProcessPair(Address),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PriorityQueue(pub Vec<Action>);
|
||||||
|
|
||||||
|
impl PriorityQueue {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PriorityQueue(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, action: Action) {
|
||||||
|
debug!("Adding action {:?} to the priority queue", action);
|
||||||
|
self.0.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for PriorityQueue {
|
||||||
|
type Item = Action;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
match self.0.pop() {
|
||||||
|
None => Poll::Ready(None),
|
||||||
|
Some(action) => {
|
||||||
|
debug!("Consuming action {:?} to the priority queue", action);
|
||||||
|
|
||||||
|
match action {
|
||||||
|
Action::ProcessPair(pair) => Poll::Ready(Some(Action::ProcessPair(pair))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
150
offchain/src/run.rs
Normal file
150
offchain/src/run.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
#[path = "pairs.rs"]
|
||||||
|
mod pairs;
|
||||||
|
#[path = "priority_queue.rs"]
|
||||||
|
mod priority_queue;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use alloy::{
|
||||||
|
eips::BlockNumberOrTag,
|
||||||
|
primitives::Address,
|
||||||
|
providers::{
|
||||||
|
fillers::FillProvider, DynProvider, Provider, ProviderBuilder, RootProvider, WsConnect,
|
||||||
|
},
|
||||||
|
pubsub::PubSubFrontend,
|
||||||
|
rpc::types::Filter,
|
||||||
|
};
|
||||||
|
use futures_util::{stream, StreamExt};
|
||||||
|
use miette::{miette, Result};
|
||||||
|
|
||||||
|
use log::{debug, info};
|
||||||
|
use pairs::Pairs;
|
||||||
|
use priority_queue::{Action, PriorityQueue};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
alloy::sol!(
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[sol(rpc)]
|
||||||
|
IUniswapV2Pair,
|
||||||
|
"abi/IUniswapV2Pair.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn process_swaps(
|
||||||
|
ws: WsConnect,
|
||||||
|
priority_queue: Arc<Mutex<PriorityQueue>>,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let provider = ProviderBuilder::new().on_ws(ws).await.unwrap();
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.event("Swap(address,uint256,uint256,uint256,uint256,address)")
|
||||||
|
.from_block(BlockNumberOrTag::Latest);
|
||||||
|
|
||||||
|
let sub = provider.subscribe_logs(&filter).await?;
|
||||||
|
let mut stream = sub.into_stream();
|
||||||
|
|
||||||
|
let mut latest_block = 0;
|
||||||
|
|
||||||
|
while let Some(log) = stream.next().await {
|
||||||
|
let block_number = log.block_number.unwrap();
|
||||||
|
if block_number > latest_block {
|
||||||
|
latest_block = block_number;
|
||||||
|
info!("Processing block number {:?}", block_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
priority_queue
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(Action::ProcessPair(log.address()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_pair(
|
||||||
|
ws: WsConnect,
|
||||||
|
pairs: Arc<Mutex<Pairs>>,
|
||||||
|
pair_address: Address,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let provider = ProviderBuilder::new().on_ws(ws).await.unwrap();
|
||||||
|
|
||||||
|
let pair = IUniswapV2Pair::new(pair_address, provider);
|
||||||
|
let token0 = pair.token0().call().await?._0;
|
||||||
|
let token1 = pair.token1().call().await?._0;
|
||||||
|
let reserve0 = pair.getReserves().call().await?.reserve0;
|
||||||
|
let reserve1 = pair.getReserves().call().await?.reserve1;
|
||||||
|
let factory = pair.factory().call().await?._0;
|
||||||
|
|
||||||
|
pairs
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.add(pair_address, token0, token1, reserve0, reserve1, factory);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn consume_priority_queue(
|
||||||
|
ws: WsConnect,
|
||||||
|
pairs: Arc<Mutex<Pairs>>,
|
||||||
|
priority_queue: Arc<Mutex<PriorityQueue>>,
|
||||||
|
config: Config,
|
||||||
|
) {
|
||||||
|
let mut guard = priority_queue.lock().unwrap();
|
||||||
|
let actions: Vec<Action> = guard.0.drain(..).collect(); //move all actions to temporary vector in order to unlock the mutex
|
||||||
|
drop(guard); //release before the expensive operation
|
||||||
|
|
||||||
|
stream::iter(actions)
|
||||||
|
.for_each_concurrent(config.concurrency, |action| {
|
||||||
|
let pairs_clone = pairs.clone();
|
||||||
|
let ws = ws.clone();
|
||||||
|
async move {
|
||||||
|
match action {
|
||||||
|
Action::ProcessPair(pair_address) => {
|
||||||
|
info!("Processing pair: {:?}", pair_address);
|
||||||
|
process_pair(ws, pairs_clone, pair_address).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn manage_interruption(pairs: Arc<Mutex<Pairs>>, config: Config) -> eyre::Result<()> {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Error receiving the signal");
|
||||||
|
info!("Program correctly interrupted by the user");
|
||||||
|
|
||||||
|
match pairs.lock().unwrap().dump(&config.pairs_file) {
|
||||||
|
Ok(_) => info!("Pairs correctly dumped to {:?}", &config.pairs_file),
|
||||||
|
Err(e) => info!("Error dumping pairs to {:?} {:?}", &config.pairs_file, e),
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(config: Config) -> Result<()> {
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let pairs = Arc::new(Mutex::new(Pairs::new(&config.pairs_file)?));
|
||||||
|
let priority_queue = Arc::new(Mutex::new(PriorityQueue::new()));
|
||||||
|
let ws = WsConnect::new(&config.endpoint);
|
||||||
|
|
||||||
|
runtime.block_on(async {
|
||||||
|
tokio::spawn(manage_interruption(pairs.clone(), config.clone()));
|
||||||
|
|
||||||
|
// process all the `Swap` events adding actions to the queue
|
||||||
|
tokio::spawn(process_swaps(ws.clone(), priority_queue.clone()));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
consume_priority_queue(ws.clone(), pairs.clone(), priority_queue.clone(), config.clone()).await;
|
||||||
|
|
||||||
|
debug!("The entire queue has been processed, waiting 100ms before checking if new actions are available...");
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
66
onchain/README.md
Normal file
66
onchain/README.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
## Foundry
|
||||||
|
|
||||||
|
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
|
||||||
|
|
||||||
|
Foundry consists of:
|
||||||
|
|
||||||
|
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
|
||||||
|
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
|
||||||
|
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
|
||||||
|
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://book.getfoundry.sh/
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gas Snapshots
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anvil
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ anvil
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cast
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cast <subcommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Help
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ forge --help
|
||||||
|
$ anvil --help
|
||||||
|
$ cast --help
|
||||||
|
```
|
6
onchain/foundry.toml
Normal file
6
onchain/foundry.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[profile.default]
|
||||||
|
src = "src"
|
||||||
|
out = "out"
|
||||||
|
libs = ["lib"]
|
||||||
|
|
||||||
|
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
89
onchain/src/ArbitrageManager.sol
Normal file
89
onchain/src/ArbitrageManager.sol
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import {IUniswapV2Pair} from "./IUniswapV2Pair.sol";
|
||||||
|
import {IERC20} from "./IERC20.sol";
|
||||||
|
|
||||||
|
contract ArbitrageManager {
|
||||||
|
uint256 constant f = 997;
|
||||||
|
|
||||||
|
function sqrt(uint256 x)
|
||||||
|
public
|
||||||
|
pure
|
||||||
|
returns (uint128)
|
||||||
|
{
|
||||||
|
if (x == 0) return 0;
|
||||||
|
else{
|
||||||
|
uint256 xx = x;
|
||||||
|
uint256 r = 1;
|
||||||
|
if (xx >= 0x100000000000000000000000000000000) { xx >>= 128; r <<= 64; }
|
||||||
|
if (xx >= 0x10000000000000000) { xx >>= 64; r <<= 32; }
|
||||||
|
if (xx >= 0x100000000) { xx >>= 32; r <<= 16; }
|
||||||
|
if (xx >= 0x10000) { xx >>= 16; r <<= 8; }
|
||||||
|
if (xx >= 0x100) { xx >>= 8; r <<= 4; }
|
||||||
|
if (xx >= 0x10) { xx >>= 4; r <<= 2; }
|
||||||
|
if (xx >= 0x8) { r <<= 1; }
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
r = (r + x / r) >> 1;
|
||||||
|
uint256 r1 = x / r;
|
||||||
|
return uint128 (r < r1 ? r : r1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
|
||||||
|
public
|
||||||
|
pure
|
||||||
|
returns (uint256 amountOut)
|
||||||
|
{
|
||||||
|
require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
|
||||||
|
require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
|
||||||
|
uint256 amountInWithFee = amountIn * 997;
|
||||||
|
uint256 numerator = amountInWithFee * reserveOut;
|
||||||
|
uint256 denominator = reserveIn * 1000 + amountInWithFee;
|
||||||
|
amountOut = numerator / denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optimalIn(uint256 X_A, uint256 Y_A, uint256 X_B, uint256 Y_B)
|
||||||
|
public
|
||||||
|
pure
|
||||||
|
returns(uint256)
|
||||||
|
{
|
||||||
|
uint256 k = f * X_B / 1000 + f ** 2 / 1000 * X_A / 1000;
|
||||||
|
uint256 phi = sqrt(f * X_A) * sqrt(Y_B * X_B / 1000 * Y_A);
|
||||||
|
uint256 psi = Y_A * X_B;
|
||||||
|
|
||||||
|
if (psi >= phi) return 0;
|
||||||
|
else return (phi - psi) / k;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(address _pairA, address _pairB, uint256 amountIn, bool direction)
|
||||||
|
external
|
||||||
|
returns (uint256 amountOut)
|
||||||
|
{
|
||||||
|
IUniswapV2Pair pairA = IUniswapV2Pair(_pairA);
|
||||||
|
IUniswapV2Pair pairB = IUniswapV2Pair(_pairB);
|
||||||
|
|
||||||
|
IERC20 tokenA = direction ? IERC20(pairA.token0()) : IERC20(pairA.token1());
|
||||||
|
|
||||||
|
// Transfer the input tokens from the sender to pairA
|
||||||
|
tokenA.transferFrom(msg.sender, address(pairA), amountIn);
|
||||||
|
|
||||||
|
// Perform the first swap on pairA
|
||||||
|
(uint256 reserve0A, uint256 reserve1A,) = pairA.getReserves();
|
||||||
|
amountOut = getAmountOut(amountIn, direction ? reserve0A : reserve1A, direction ? reserve1A : reserve0A);
|
||||||
|
pairA.swap(direction ? 0 : amountOut, direction ? amountOut : 0, address(pairB), new bytes(0));
|
||||||
|
|
||||||
|
// Perform the second swap on pairB
|
||||||
|
(uint256 reserve0B, uint256 reserve1B,) = pairB.getReserves();
|
||||||
|
amountOut = getAmountOut(amountOut, direction ? reserve1B : reserve0B, direction ? reserve0B : reserve1B);
|
||||||
|
pairB.swap(direction ? amountOut : 0, direction ? 0 : amountOut, msg.sender, new bytes(0));
|
||||||
|
|
||||||
|
// Ensure that the arbitrage is profitable
|
||||||
|
require(amountOut > amountIn, "Arbitrage not profitable");
|
||||||
|
}
|
||||||
|
}
|
17
onchain/src/IERC20.sol
Normal file
17
onchain/src/IERC20.sol
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
interface IERC20 {
|
||||||
|
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||||
|
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||||
|
|
||||||
|
function name() external view returns (string memory);
|
||||||
|
function symbol() external view returns (string memory);
|
||||||
|
function decimals() external view returns (uint8);
|
||||||
|
function totalSupply() external view returns (uint256);
|
||||||
|
function balanceOf(address owner) external view returns (uint256);
|
||||||
|
function allowance(address owner, address spender) external view returns (uint256);
|
||||||
|
|
||||||
|
function approve(address spender, uint256 value) external returns (bool);
|
||||||
|
function transfer(address to, uint256 value) external returns (bool);
|
||||||
|
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
||||||
|
}
|
53
onchain/src/IUniswapV2Pair.sol
Normal file
53
onchain/src/IUniswapV2Pair.sol
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
interface IUniswapV2Pair {
|
||||||
|
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||||
|
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||||
|
|
||||||
|
function name() external pure returns (string memory);
|
||||||
|
function symbol() external pure returns (string memory);
|
||||||
|
function decimals() external pure returns (uint8);
|
||||||
|
function totalSupply() external view returns (uint256);
|
||||||
|
function balanceOf(address owner) external view returns (uint256);
|
||||||
|
function allowance(address owner, address spender) external view returns (uint256);
|
||||||
|
|
||||||
|
function approve(address spender, uint256 value) external returns (bool);
|
||||||
|
function transfer(address to, uint256 value) external returns (bool);
|
||||||
|
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
||||||
|
|
||||||
|
function DOMAIN_SEPARATOR() external view returns (bytes32);
|
||||||
|
function PERMIT_TYPEHASH() external pure returns (bytes32);
|
||||||
|
function nonces(address owner) external view returns (uint256);
|
||||||
|
|
||||||
|
function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
|
||||||
|
external;
|
||||||
|
|
||||||
|
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
|
||||||
|
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
|
||||||
|
event Swap(
|
||||||
|
address indexed sender,
|
||||||
|
uint256 amount0In,
|
||||||
|
uint256 amount1In,
|
||||||
|
uint256 amount0Out,
|
||||||
|
uint256 amount1Out,
|
||||||
|
address indexed to
|
||||||
|
);
|
||||||
|
event Sync(uint112 reserve0, uint112 reserve1);
|
||||||
|
|
||||||
|
function MINIMUM_LIQUIDITY() external pure returns (uint256);
|
||||||
|
function factory() external view returns (address);
|
||||||
|
function token0() external view returns (address);
|
||||||
|
function token1() external view returns (address);
|
||||||
|
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
|
||||||
|
function price0CumulativeLast() external view returns (uint256);
|
||||||
|
function price1CumulativeLast() external view returns (uint256);
|
||||||
|
function kLast() external view returns (uint256);
|
||||||
|
|
||||||
|
function mint(address to) external returns (uint256 liquidity);
|
||||||
|
function burn(address to) external returns (uint256 amount0, uint256 amount1);
|
||||||
|
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
|
||||||
|
function skim(address to) external;
|
||||||
|
function sync() external;
|
||||||
|
|
||||||
|
function initialize(address, address) external;
|
||||||
|
}
|
77
onchain/test/ArbitrageManager.t.sol
Normal file
77
onchain/test/ArbitrageManager.t.sol
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import {Test, console} from "forge-std/Test.sol";
|
||||||
|
import {ArbitrageManager} from "../src/ArbitrageManager.sol";
|
||||||
|
import {IERC20} from "../src/IERC20.sol";
|
||||||
|
import {IUniswapV2Pair} from "../src/IUniswapV2Pair.sol";
|
||||||
|
|
||||||
|
|
||||||
|
contract ArbitrageTest is Test {
|
||||||
|
ArbitrageManager public arbitrageManager;
|
||||||
|
uint256 mainnetFork;
|
||||||
|
address constant vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
|
||||||
|
IERC20 weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||||
|
IUniswapV2Pair uniswapPair = IUniswapV2Pair(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc);
|
||||||
|
IUniswapV2Pair sushiswapPair = IUniswapV2Pair(0x397FF1542f962076d0BFE58eA045FfA2d347ACa0);
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
mainnetFork = vm.createFork("https://eth-mainnet.g.alchemy.com/v2/kkDMaLVYpWQA0GsCYNFvAODnAxCCiamv"); // TODO use an env variable
|
||||||
|
vm.selectFork(mainnetFork);
|
||||||
|
vm.rollFork(22_147_269);
|
||||||
|
arbitrageManager = new ArbitrageManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_canSelectFork() public view {
|
||||||
|
assertEq(vm.activeFork(), mainnetFork);
|
||||||
|
assertEq(block.number, 22_147_269);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_sqrt() public view {
|
||||||
|
uint256 n = 344353442342354234324324324323;
|
||||||
|
assertEq(arbitrageManager.sqrt(n**2), n);
|
||||||
|
|
||||||
|
n = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
|
||||||
|
assertEq(340282366920938463463374607431768211456 - 1, arbitrageManager.sqrt(n)); // +-1 is an acceptable rounding error
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_computeAmountIn() public {
|
||||||
|
(uint256 X_A, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH)
|
||||||
|
(uint256 X_B, uint256 Y_B, ) = sushiswapPair.getReserves(); // (USDT, WETH)
|
||||||
|
|
||||||
|
console.log("Uniswap pair reserves", X_A, Y_A);
|
||||||
|
console.log("Uniswap pair ratio", Y_A/X_A);
|
||||||
|
console.log("Sushiswap pair ratio", Y_B/X_B);
|
||||||
|
|
||||||
|
uint256 unbalance = Y_A / 5;
|
||||||
|
console.log("Unbalance", unbalance);
|
||||||
|
vm.prank(address(uniswapPair)); // it works only for the next call
|
||||||
|
weth.transfer(address(0), unbalance);
|
||||||
|
uniswapPair.sync();
|
||||||
|
console.log("Arbitrage opportunity created");
|
||||||
|
|
||||||
|
(X_A, Y_A, ) = uniswapPair.getReserves();
|
||||||
|
(X_B, Y_B, ) = sushiswapPair.getReserves();
|
||||||
|
console.log("Uniswap pair reserves", X_A, Y_A);
|
||||||
|
console.log("Uniswap pair ratio", Y_A/X_A);
|
||||||
|
console.log("Sushiswap pair ratio", Y_B/X_B);
|
||||||
|
|
||||||
|
uint256 optimum = arbitrageManager.optimalIn(X_A, Y_A, X_B, Y_B);
|
||||||
|
console.log("The optimum is", optimum);
|
||||||
|
|
||||||
|
vm.prank(address(0)); // it works only for the next call
|
||||||
|
weth.transfer(address(uniswapPair), optimum);
|
||||||
|
uint256 amountOut = arbitrageManager.getAmountOut(optimum, Y_A, X_A);
|
||||||
|
console.log("First swap's amountOut", amountOut);
|
||||||
|
uniswapPair.swap(amountOut, 0, address(sushiswapPair), new bytes(0));
|
||||||
|
amountOut = arbitrageManager.getAmountOut(amountOut, X_B, Y_B);
|
||||||
|
console.log("Second swap's amountOut", amountOut);
|
||||||
|
sushiswapPair.swap(0, amountOut, address(this), new bytes(0));
|
||||||
|
|
||||||
|
(X_A, Y_A, ) = uniswapPair.getReserves();
|
||||||
|
(X_B, Y_B, ) = sushiswapPair.getReserves();
|
||||||
|
console.log("Uniswap pair reserves", X_A, Y_A);
|
||||||
|
console.log("Uniswap pair ratio", Y_A/X_A);
|
||||||
|
console.log("Sushiswap pair ratio", Y_B/X_B);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue