From fb378c4931ad37fe4634c8426b138d5b4ddeb4dd Mon Sep 17 00:00:00 2001 From: Andrea Ciceri Date: Sat, 10 May 2025 10:45:59 +0200 Subject: [PATCH] Add `typst` and `tinymist` to shell Start writing notes Notes on Uniswap V2's optimum Fix error in formula Test `computeAmountInt` using various deltas Add `concurrency` to the default configuration file Remove unused imports Correctly propagate error Allow dead code Make the priority queue a real FIFO Refactor: remove priority queue as stream and use channels Increase buffer size New `flashArbitrage` function Comment with some ideas Add pragma version Refactor: decrease the amount of calls Remove unused code Re-enable tests Remove comment Process known pairs when started Avoid re-allocating a new provider every time Ignore `nixos.qcow2` file created by the VM Add support for `aarch64-linux` Add NixOS module and VM configuration Add `itertools` Add arbitrage opportunity detection Implement `fallback` method for non standard callbacks Add more logs Fix sign error in optimum formula Add deployment scripts and `agenix-shell` secrets Bump cargo packages Fix typo Print out an error if processing a pair goes wrong Add `actionlint` to formatters Fix typo Add TODO comment Remove not relevant anymore comment Big refactor - process actions always in the correct order avoiding corner cases - avoid using semaphores New API key Add `age` to dev shell Used by Emacs' `agenix-mode` on my system Fix parametric deploy scripts Add `run-forge-tests` flake app Remove fork URL from Solidity source Remove `pairDir` argument Add link to `ArbitrageManager`'s ABI WIP --- .gitignore | 3 +- flake.lock | 211 ++++++++++- flake.nix | 178 ++++++++- notes/notes.pdf | Bin 0 -> 34602 bytes notes/notes.typ | 86 +++++ offchain/Cargo.lock | 569 +++++++++++++++------------- offchain/Cargo.toml | 1 + offchain/abi/ArbitrageManager.json | 1 + offchain/src/pairs.rs | 136 ++++++- offchain/src/priority_queue.rs | 44 --- offchain/src/run.rs | 290 ++++++++++---- onchain/src/ArbitrageManager.sol | 65 +++- onchain/src/IUniswapV2Callee.sol | 5 + onchain/test/ArbitrageManager.t.sol | 50 ++- secrets/alchemy_key.age | 5 + secrets/secrets.nix | 7 + secrets/wallet_private_key.age | 12 + 17 files changed, 1222 insertions(+), 441 deletions(-) create mode 100644 notes/notes.pdf create mode 100644 notes/notes.typ create mode 120000 offchain/abi/ArbitrageManager.json delete mode 100644 offchain/src/priority_queue.rs create mode 100644 onchain/src/IUniswapV2Callee.sol create mode 100644 secrets/alchemy_key.age create mode 100644 secrets/secrets.nix create mode 100644 secrets/wallet_private_key.age diff --git a/.gitignore b/.gitignore index f7cb527..1869051 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ onchain/out onchain/cache .direnv .pre-commit-config.yaml -**/result \ No newline at end of file +**/result +nixos.qcow2 \ No newline at end of file diff --git a/flake.lock b/flake.lock index 80eb92d..03ab6c9 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,28 @@ { "nodes": { + "agenix-shell": { + "inputs": { + "flake-parts": "flake-parts", + "flake-root": "flake-root", + "git-hooks-nix": "git-hooks-nix", + "nix-github-actions": "nix-github-actions", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1747248416, + "narHash": "sha256-mthvi7EARHz01rqyJEvyZtrXooKEEoLkt7Fhu2W1djM=", + "owner": "aciceri", + "repo": "agenix-shell", + "rev": "df2787101d5feb8f82e50d100ad37fc0b6c53b75", + "type": "github" + }, + "original": { + "owner": "aciceri", + "repo": "agenix-shell", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -16,10 +39,44 @@ "type": "github" } }, + "flake-compat_2": { + "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": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, "locked": { "lastModified": 1741352980, "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=", @@ -49,6 +106,21 @@ "type": "github" } }, + "flake-root_2": { + "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": { @@ -68,8 +140,8 @@ }, "git-hooks": { "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", + "flake-compat": "flake-compat_2", + "gitignore": "gitignore_2", "nixpkgs": [ "nixpkgs" ] @@ -88,7 +160,52 @@ "type": "github" } }, + "git-hooks-nix": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "agenix-shell", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746537231, + "narHash": "sha256-Wb2xeSyOsCoTCTj7LOoD6cdKLEROyFAArnYoS+noCWo=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "fa466640195d38ec97cf0493d6d6882bc4d14969", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "gitignore": { + "inputs": { + "nixpkgs": [ + "agenix-shell", + "git-hooks-nix", + "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" + } + }, + "gitignore_2": { "inputs": { "nixpkgs": [ "git-hooks", @@ -110,6 +227,27 @@ } }, "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "agenix-shell", + "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" + } + }, + "nix-github-actions_2": { "inputs": { "nixpkgs": [ "nixpkgs" @@ -131,11 +269,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1742669843, - "narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=", + "lastModified": 1746663147, + "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1e5b653dff12029333a6546c11e108ede13052eb", + "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", "type": "github" }, "original": { @@ -146,6 +284,21 @@ } }, "nixpkgs-lib": { + "locked": { + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { "locked": { "lastModified": 1740877520, "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=", @@ -160,18 +313,56 @@ "type": "github" } }, + "nixpkgs_2": { + "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" + } + }, "root": { "inputs": { - "flake-parts": "flake-parts", - "flake-root": "flake-root", + "agenix-shell": "agenix-shell", + "flake-parts": "flake-parts_2", + "flake-root": "flake-root_2", "forge-std": "forge-std", "git-hooks": "git-hooks", - "nix-github-actions": "nix-github-actions", - "nixpkgs": "nixpkgs", - "treefmt-nix": "treefmt-nix" + "nix-github-actions": "nix-github-actions_2", + "nixpkgs": "nixpkgs_2", + "treefmt-nix": "treefmt-nix_2" } }, "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "agenix-shell", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746216483, + "narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "29ec5026372e0dec56f890e50dbe4f45930320fd", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "treefmt-nix_2": { "inputs": { "nixpkgs": [ "nixpkgs" diff --git a/flake.nix b/flake.nix index ddf89a1..f1e1dab 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,7 @@ url = "github:cachix/git-hooks.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + agenix-shell.url = "github:aciceri/agenix-shell"; flake-root.url = "github:srid/flake-root"; nix-github-actions = { url = "github:nix-community/nix-github-actions"; @@ -22,15 +23,23 @@ }; outputs = inputs: - inputs.flake-parts.lib.mkFlake { inherit inputs; } ({ config, lib, ... }: { - systems = [ "x86_64-linux" ]; + inputs.flake-parts.lib.mkFlake { inherit inputs; } (flake@{ config, lib, moduleWithSystem, withSystem, ... }: { + systems = [ "x86_64-linux" "aarch64-linux" ]; - imports = [ - inputs.git-hooks.flakeModule - inputs.treefmt-nix.flakeModule - inputs.flake-root.flakeModule + imports = with inputs; [ + git-hooks.flakeModule + treefmt-nix.flakeModule + flake-root.flakeModule + agenix-shell.flakeModules.agenix-shell ]; + agenix-shell = { + secrets = { + ALCHEMY_KEY.file = ./secrets/alchemy_key.age; + WALLET_PRIVATE_KEY.file = ./secrets/wallet_private_key.age; + }; + }; + perSystem = { pkgs, config, ... }: { treefmt.config = { flakeFormatter = true; @@ -38,6 +47,7 @@ programs = { nixpkgs-fmt.enable = true; rustfmt.enable = true; + actionlint.enable = true; }; }; @@ -52,9 +62,11 @@ }; devShells.default = pkgs.mkShell { - packages = with pkgs; [ cargo rustc rust-analyzer clippy foundry ]; + packages = with pkgs; [ cargo rustc rust-analyzer clippy foundry typst tinymist age ragenix ]; inputsFrom = [ config.flake-root.devShell ]; shellHook = '' + source ${lib.getExe config.agenix-shell.installationScript} + # forge will use this directory to download the solc compilers mkdir -p $HOME/.svm @@ -95,21 +107,167 @@ OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include"; PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"; }; + meta.mainProgram = "arbi"; }; arbi_sample_config_kdl = pkgs.writeText "arbi-sample-config.kdl" '' endpoint "wss://eth-mainnet.g.alchemy.com/v2/" pairs_file "pairs.json" + concurrency 5 ''; - }; + + run-forge-tests = pkgs.writeShellScriptBin "run-forge-tests" '' + pushd "$FLAKE_ROOT/onchain" + forge test \ + --fork-url "wss://mainnet.infura.io/ws/v3/$ALCHEMY_KEY" \ + --via-ir \ + -vvv + popd + ''; + + run-vm = pkgs.writeShellScriptBin "run-vm" (lib.getExe flake.config.flake.nixosConfigurations.vm.config.system.build.vm); + } // lib.genAttrs [ "polygon-mainnet" ] (network: pkgs.writeShellScriptBin "deploy-${network}" '' + pushd "$FLAKE_ROOT/onchain" + forge create \ + --rpc-url "wss://${network}.infura.io/ws/v3/$ALCHEMY_KEY" \ + --private-key "$WALLET_PRIVATE_KEY" \ + --via-ir \ + --broadcast \ + src/ArbitrageManager.sol:ArbitrageManager + popd + ''); + checks = { inherit (config.packages) arbi; }; }; - flake.githubActions = inputs.nix-github-actions.lib.mkGithubMatrix { - checks = lib.getAttrs [ "x86_64-linux" ] config.flake.checks; + flake = { + githubActions = inputs.nix-github-actions.lib.mkGithubMatrix { + checks = lib.getAttrs [ "x86_64-linux" ] config.flake.checks; + }; + + nixosConfigurations.vm = withSystem "x86_64-linux" (ctx: inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ({ pkgs, modulesPath, ... }: { + imports = [ + "${modulesPath}/virtualisation/qemu-vm.nix" + config.flake.nixosModules.arbi + ]; + + services.getty.autologinUser = "root"; + services.openssh.settings.PasswordAuthentication = lib.mkForce true; + services.openssh.settings.PermitRootLogin = lib.mkForce "yes"; + users.users.root.password = ""; + + virtualisation = { + graphics = false; + memorySize = 2048; + diskSize = 10000; + forwardPorts = [ + { + from = "host"; + host.port = 2222; + guest.port = 22; + } + ]; + }; + + system.stateVersion = "25.05"; + + services.arbi = { + enable = true; + log_level = "debug"; + configFile = pkgs.writeText "arbi-config.kdl" '' + endpoint "wss://eth-mainnet.g.alchemy.com/v2/kkDMaLVYpWQA0GsCYNFvAODnAxCCiamv" + pairs_file "pairs.json" + concurrency 5 + ''; + }; + }) + ]; + }); + + nixosModules = { + arbi = moduleWithSystem ({ config }: nixos@{ lib, utils, ... }: + let + cfg = nixos.config.services.arbi; + in + { + options.services.arbi = { + enable = lib.mkEnableOption "arbi"; + package = lib.mkOption { + type = lib.types.package; + default = config.packages.arbi; + }; + log_level = lib.mkOption { + type = lib.types.enum [ "debug" "trace" "warn" "error" "info" ]; + default = "info"; + }; + configFile = lib.mkOption { + type = lib.types.path; + }; + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/arbi"; + }; + user = lib.mkOption { + type = lib.types.str; + default = "arbi"; + }; + group = lib.mkOption { + type = lib.types.str; + default = "arbi"; + }; + }; + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + users.users.arbi = lib.mkIf (cfg.user == "arbi") { + isSystemUser = true; + group = cfg.group; + }; + + users.groups.arbi = lib.mkIf (cfg.group == "arbi") { }; + + systemd.tmpfiles.settings."10-arbi" = { + ${cfg.dataDir}.d = { + inherit (cfg) user group; + mode = "0755"; + }; + }; + + systemd.services.arbi = { + description = "Arbitrage bot"; + + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment.ARBI_LOG_LEVEL = cfg.log_level; + + serviceConfig = { + ExecStart = utils.escapeSystemdExecArgs [ + (lib.getExe cfg.package) + "--config" + cfg.configFile + "run" + ]; + + KillSignal = "SIGINT"; + Restart = "on-failure"; + RestartSec = "5s"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + UMask = "0022"; + }; + }; + }; + }); + default = config.flake.nixosModules.arbi; + }; }; }); } diff --git a/notes/notes.pdf b/notes/notes.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7384bf5fd439a06be2ef6cc75bc9e79fa47ea50b GIT binary patch literal 34602 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 z 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 A ->^(x_"out") B ->^(y_"out") ... +$ + +with $y^*$ the optimum amount to swap in order to maximize the gain function $G(y) = y_"out" - y^*$ + +Let $0 <= f <= 1$ be the fee ($.03$ by deault on Uniswap V2), we know#footnote[https://www.youtube.com/watch?v=9EKksG-fF1k] that the optimum is one of the roots of the following second-grade equation: + +$ + k^2y^2 + 2k Y_A X_B y + (Y_A X_B)^2 - (1-f)^2 X_A Y_B Y_A X_B = 0 +$ + +where + +$ + k = (1-f)X_B + (1-f)^2 X_A +$ + +In the Uniswap V2 implementation we have that $1-f = phi/1000$ (with $phi = 997$). +Then we can rewrite: + +$ + k^2y^2 + 2k Y_A X_B y + (Y_A X_B)^2 - (phi/1000)^2 X_A Y_B Y_A X_B = 0 +$ + +and + +$ + k = phi/1000 X_B + phi^2/1000^2 X_A +$ + +Let $a$, $b$ and $c$ be the three second-grade equation coefficients. + +$ + a = k^2 +$ + +$ + b = 2k Y_A X_B +$ + +$ + c = (Y_A X_B)^2 - (phi/1000)^2 X_A Y_B Y_A X_B +$ + +Since $b$ is even we can find the roots with + +$ + y_i = (-b/2 plus.minus sqrt((b^2-4a c)/4))/a +$ + +Replacing our values: + +$ + (- k Y_A X_B y plus.minus sqrt(k^2 (Y_A X_B) ^2 ((Y_A X_B) ^2 -phi^2/1000^2 X_A Y_B X_B Y_A)))/k^2 +$ +$ + = -(Y_A X_B)/k plus.minus 1/k^2 sqrt(k^2 ((Y_A X_B)^2) -(Y_A X_B)^2 + phi^2/1000^2X_A Y_B X_B Y_A) +$ +$ + = -(Y_A X_B)/k plus.minus 1/k sqrt((phi^2 X_B Y_B X_B Y_A )/1000^2) +$ + +Which, since the square root is positive, can be positive only considering $+$. In conclusion we get the following formula for the optimal amount of token $Y$: + +$ + y^* = 1/k (sqrt((phi^2 X_A Y_B X_B Y_A) / 1000^2) - Y_A X_B) +$ + +=== Solidity implementation details + +- Integer square roots can be effectively and cheaply computed using the Babylonian method #footnote[https://ethereum.stackexchange.com/a/97540/66173] +- The square root can lead to overflow, in that case it can be convenient splitting it into something like +$ + sqrt(phi times X_A div 1000 times Y_B) sqrt(phi times X_B div 1000 times Y_A) +$ diff --git a/offchain/Cargo.lock b/offchain/Cargo.lock index d756d81..6345990 100644 --- a/offchain/Cargo.lock +++ b/offchain/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.67" +version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826a55d29df2765ec264190cd1c26dd1d6232ef811825024a25cdf5eaaadbdf" +checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" dependencies = [ "alloy-primitives", "num_enum", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23ccdb29eedfa1d83f32efbc958d0944e6928e252295dd5eafc516ed57f3a0a" +checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada55b5ab26624766bb8c65f72516dee93eaf28d5d87fc18ff4324cd8c2a948d" +checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -154,7 +154,7 @@ dependencies = [ "itoa", "serde", "serde_json", - "winnow 0.7.4", + "winnow 0.7.10", ] [[package]] @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df4054f177d1600f17e2bc152f6a927592641b19861e6005cc51bdf7d4fa27a6" +checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7283185baefbe66136649dc316c9dcc6f0e9f1d635ae19783615919f83bc298a" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "bytes", @@ -305,8 +305,8 @@ dependencies = [ "const-hex", "derive_more 2.0.1", "foldhash", - "hashbrown 0.15.2", - "indexmap 2.8.0", + "hashbrown 0.15.3", + "indexmap 2.9.0", "itoa", "k256", "keccak-asm", @@ -402,7 +402,7 @@ checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -589,42 +589,42 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b007e002f1082b28827cc47d9c72562d412a98c06f29aa438118ff3036c43" +checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c0a9cb9b1afbcd3325e0fff9fdf98e6d095643fae9e5584e80597f0b79b6d6e" +checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.8.0", + "indexmap 2.9.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "530c4863e707b95f99b37792cdfa94d30004ec552aed41e200a1d9264d44e669" +checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" dependencies = [ "alloy-json-abi", "const-hex", @@ -634,25 +634,25 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.100", + "syn 2.0.101", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b210dd863afa9da93c488601a1f23bee1e3ce47e15519582320c205645a7a0" +checksum = "6d162f8524adfdfb0e4bd0505c734c985f3e2474eb022af32eef0d52a4f3935c" dependencies = [ "serde", - "winnow 0.7.4", + "winnow 0.7.10", ] [[package]] name = "alloy-sol-types" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5ff802859e2797d022dc812b5b4ee40d829e0fb446c269a87826c7f0021976" +checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -826,6 +826,7 @@ dependencies = [ "env_logger", "eyre", "futures-util", + "itertools 0.14.0", "kdl", "log", "miette", @@ -986,7 +987,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -997,7 +998,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1013,13 +1014,13 @@ dependencies = [ [[package]] name = "auto_impl" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1030,9 +1031,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -1174,9 +1175,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ "shlex", ] @@ -1189,9 +1190,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1202,9 +1203,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", "clap_derive", @@ -1212,9 +1213,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -1231,7 +1232,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1312,9 +1313,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -1361,9 +1362,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -1371,27 +1372,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1410,15 +1411,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", @@ -1426,9 +1427,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1471,7 +1472,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1482,7 +1483,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "unicode-xid", ] @@ -1515,7 +1516,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1586,9 +1587,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -1605,9 +1606,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1771,7 +1772,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1823,9 +1824,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1834,9 +1835,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", @@ -1881,9 +1882,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -1998,9 +1999,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2008,6 +2009,7 @@ dependencies = [ "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2017,9 +2019,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2041,21 +2043,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -2064,31 +2067,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -2096,67 +2079,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2176,9 +2146,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -2201,7 +2171,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2223,12 +2193,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -2291,9 +2261,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.5" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", "log", @@ -2304,13 +2274,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.5" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2376,27 +2346,27 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" @@ -2420,7 +2390,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -2431,7 +2401,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2442,9 +2412,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miette" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "backtrace", "backtrace-ext", @@ -2456,19 +2426,18 @@ dependencies = [ "supports-unicode", "terminal_size", "textwrap", - "thiserror 1.0.69", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2479,9 +2448,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -2621,7 +2590,7 @@ checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2648,15 +2617,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags", "cfg-if", @@ -2675,7 +2644,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2686,9 +2655,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -2727,7 +2696,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2803,7 +2772,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2849,6 +2818,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2903,14 +2881,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -2976,13 +2954,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy", ] [[package]] @@ -3011,7 +2988,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -3020,7 +2997,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -3040,9 +3017,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags", ] @@ -3134,7 +3111,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -3169,7 +3146,7 @@ dependencies = [ "primitive-types", "proptest", "rand 0.8.5", - "rand 0.9.0", + "rand 0.9.1", "rlp", "ruint-macro", "serde", @@ -3221,9 +3198,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", @@ -3234,9 +3211,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", @@ -3257,15 +3234,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -3396,7 +3376,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3433,7 +3413,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -3450,7 +3430,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3476,9 +3456,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3513,9 +3493,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3541,18 +3521,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3605,7 +3585,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3648,9 +3628,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3659,14 +3639,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36dbbf0d465ab9fdfea3093e755ae8839bdc1263dbe18d35064d02d6060f949e" +checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3680,13 +3660,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3697,12 +3677,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3754,7 +3734,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3765,7 +3745,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3819,9 +3799,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3829,9 +3809,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -3853,7 +3833,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3901,14 +3881,14 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -3919,19 +3899,19 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "toml_datetime", - "winnow 0.7.4", + "winnow 0.7.10", ] [[package]] @@ -3980,7 +3960,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4021,7 +4001,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.0", + "rand 0.9.1", "rustls", "rustls-pki-types", "sha1", @@ -4112,12 +4092,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4203,7 +4177,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -4238,7 +4212,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4278,9 +4252,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] @@ -4293,11 +4276,37 @@ checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -4313,7 +4322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -4335,6 +4344,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4492,9 +4510,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -4508,17 +4526,11 @@ dependencies = [ "bitflags", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" @@ -4550,9 +4562,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4562,34 +4574,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4609,7 +4621,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -4630,14 +4642,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4646,11 +4669,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] diff --git a/offchain/Cargo.toml b/offchain/Cargo.toml index d2c2685..d922c3a 100644 --- a/offchain/Cargo.toml +++ b/offchain/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.5.32", features = ["derive", "env"] } env_logger = "0.11.7" eyre = "0.6.12" futures-util = "0.3.31" +itertools = "0.14.0" kdl = "6.3.4" log = "0.4.27" miette = { version = "7.5.0", features = ["fancy"] } diff --git a/offchain/abi/ArbitrageManager.json b/offchain/abi/ArbitrageManager.json new file mode 120000 index 0000000..accb340 --- /dev/null +++ b/offchain/abi/ArbitrageManager.json @@ -0,0 +1 @@ +../../onchain/out/ArbitrageManager.sol/ArbitrageManager.json \ No newline at end of file diff --git a/offchain/src/pairs.rs b/offchain/src/pairs.rs index 09ab735..771bf6d 100644 --- a/offchain/src/pairs.rs +++ b/offchain/src/pairs.rs @@ -1,7 +1,9 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; +use alloy::primitives::U256; use alloy::primitives::{aliases::U112, Address}; +use itertools::Itertools; use miette::{miette, Result}; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; @@ -10,11 +12,11 @@ use log::{debug, info}; #[derive(Debug, Serialize, Deserialize)] pub struct Pair { - token0: Address, - token1: Address, - reserve0: U112, - reserve1: U112, - factory: Address, + pub token0: Address, + pub token1: Address, + pub reserve0: U112, + pub reserve1: U112, + pub factory: Address, } #[derive(Debug, Eq, Hash, PartialEq)] @@ -68,6 +70,14 @@ impl<'de> Deserialize<'de> for AddressPair { } } +#[derive(Debug)] +pub struct ArbitrageOpportunity { + pair_a: Address, + pair_b: Address, + direction: bool, // true means token0 -> token1 -> token0 + optimum: U256, +} + #[derive(Debug, Serialize, Deserialize)] pub struct Pairs { pairs: HashMap, @@ -97,10 +107,17 @@ impl Pairs { } } - fn get(&self, address: Address) -> Option<&Pair> { + #[allow(dead_code)] + pub fn get(&self, address: Address) -> Option<&Pair> { self.pairs.get(&address) } + pub fn get_tokens(&self, address: Address) -> Option<(Address, Address)> { + self.pairs + .get(&address) + .map(|pair| (pair.token0, pair.token1)) + } + pub fn add( &mut self, pair: Address, @@ -133,11 +150,11 @@ impl Pairs { info!("First time seeing pair {}, adding it", { pair }); match self.by_tokens.get_mut(&AddressPair(token0, token1)) { - Some(tokens) => { - tokens.push(pair); + Some(pairs) => { + pairs.push(pair); info!( "Already know {} pairs with tokens {:?} and {:?}", - tokens.len(), + pairs.len(), token0, token1 ); @@ -161,6 +178,107 @@ impl Pairs { let data = serde_json::to_string(&self).map_err(|e| miette!(e))?; std::fs::write(filename, data).map_err(|e| miette!(e))?; + info!("{} Pairs saved to {:?}", self.pairs.len(), filename); + Ok(()) } + + pub fn len(&self) -> usize { + self.pairs.len() + } + + pub fn iter(&self) -> std::collections::hash_map::Iter<'_, Address, Pair> { + self.pairs.iter() + } + + pub fn get_addresses(&self) -> Vec
{ + self.pairs.keys().cloned().collect() + } + + pub fn get_reserves(&self, address: Address) -> Option<(U112, U112)> { + self.pairs + .get(&address) + .map(|pair| (pair.reserve0, pair.reserve1)) + } + + pub fn update_reserves( + &mut self, + address: Address, + reserve0: U112, + reserve1: U112, + ) -> Result<()> { + if let Some(pair) = self.pairs.get_mut(&address) { + pair.reserve0 = reserve0; + pair.reserve1 = reserve1; + info!( + "Updated reserves for pair {}: reserve0: {}, reserve1: {}", + address, reserve0, reserve1 + ); + Ok(()) + } else { + debug!("Pair {} not found", address); + Ok(()) // TODO return Err + } + } + + // TODO at the moment we return all the opportunities, instead we should return only the two opportunities + // (token0 -> token1 -> token0 and token1 -> token0 -> token1) with the highest amountIn + // Remember: choosing an opportunity invalidates the other ones + pub fn look_for_opportunity( + &self, + token0: Address, + token1: Address, + ) -> Vec { + let mut opportunities: Vec = Vec::new(); + if let Some(pairs) = self.by_tokens.get(&AddressPair(token0, token1)) { + pairs.iter() + .permutations(2) + .any(|pairs| { + let pair_a = self.get(*pairs[0]).unwrap(); + let pair_b = self.get(*pairs[1]).unwrap(); + + if let Some(optimum) = optimal_in(pair_a.reserve0, pair_a.reserve1, pair_b.reserve0, pair_b.reserve1) { + info!("Found arbitrage opportunity between pairs {} and {} swapping {} along token0 -> token1 -> token0", pairs[0], pairs[1], optimum); + opportunities.push(ArbitrageOpportunity{ + pair_a: *pairs[0], + pair_b: *pairs[1], + direction: true, + optimum + }); + } + if let Some(optimum) = optimal_in(pair_a.reserve1, pair_a.reserve0, pair_b.reserve1, pair_b.reserve0) { + info!("Found arbitrage opportunity between pairs {} and {} swapping {} along token1 -> token0 -> token1", pairs[0], pairs[1], optimum); + opportunities.push(ArbitrageOpportunity{ + pair_a: *pairs[0], + pair_b: *pairs[1], + direction: false, + optimum + }); + } + false + }); + } + opportunities + } +} + +fn optimal_in(x_a: U112, y_a: U112, x_b: U112, y_b: U112) -> Option { + let x_a = U256::from(x_a); + let x_b = U256::from(x_b); + let y_a = U256::from(y_a); + let y_b = U256::from(y_b); + let f = U256::from(997); + let ff = f.pow(U256::from(2)); + let _1000 = U256::from(1000); + let _1000000 = U256::from(1000000); + + let k = f * x_b / _1000 + ff / _1000 * x_a / _1000; + let phi = (ff * x_a * y_b * x_b * y_a / _1000000).root(2); + let psi = y_a * x_b; + + if psi >= phi { + None + } else { + Some((phi - psi) / k) + } } diff --git a/offchain/src/priority_queue.rs b/offchain/src/priority_queue.rs deleted file mode 100644 index 958eb8e..0000000 --- a/offchain/src/priority_queue.rs +++ /dev/null @@ -1,44 +0,0 @@ -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); - -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> { - 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))), - } - } - } - } -} diff --git a/offchain/src/run.rs b/offchain/src/run.rs index d27199e..1e2a01f 100644 --- a/offchain/src/run.rs +++ b/offchain/src/run.rs @@ -1,30 +1,28 @@ -#[path = "pairs.rs"] -mod pairs; -#[path = "priority_queue.rs"] -mod priority_queue; - -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +#![allow(clippy::too_many_arguments)] use crate::config::Config; use alloy::{ eips::BlockNumberOrTag, - primitives::Address, + primitives::{aliases::U112, Address, U256}, providers::{ - fillers::FillProvider, DynProvider, Provider, ProviderBuilder, RootProvider, WsConnect, + fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller}, + Provider, ProviderBuilder, RootProvider, WsConnect, }, - pubsub::PubSubFrontend, - rpc::types::Filter, + rpc::{client::RpcClient, types::Filter}, + transports::layers::RetryBackoffLayer, }; -use futures_util::{stream, StreamExt}; -use miette::{miette, Result}; - +use futures_util::StreamExt; use log::{debug, info}; +use miette::miette; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +#[path = "pairs.rs"] +mod pairs; use pairs::Pairs; -use priority_queue::{Action, PriorityQueue}; -use tokio::time::sleep; +use tokio::sync::mpsc; alloy::sol!( #[allow(missing_docs)] @@ -33,14 +31,27 @@ alloy::sol!( "abi/IUniswapV2Pair.json" ); -async fn process_swaps( - ws: WsConnect, - priority_queue: Arc>, -) -> eyre::Result<()> { - let provider = ProviderBuilder::new().on_ws(ws).await.unwrap(); +#[derive(Debug)] +pub enum Action { + ProcessNewPair(Address), + ProcessOldPair(Address, U256, U256, U256, U256), +} +type AlloyProvider = FillProvider< + JoinFill< + alloy::providers::Identity, + JoinFill>>, + >, + RootProvider, +>; + +async fn subscribe( + provider: Arc, + pairs: Arc>, + tx: mpsc::Sender, +) -> eyre::Result<()> { let filter = Filter::new() - .event("Swap(address,uint256,uint256,uint256,uint256,address)") + .event("Swap(address,uint256,uint256,uint256,uint256,address)") // TODO manage also sync and skim .from_block(BlockNumberOrTag::Latest); let sub = provider.subscribe_logs(&filter).await?; @@ -55,61 +66,130 @@ async fn process_swaps( info!("Processing block number {:?}", block_number); } - priority_queue - .lock() - .unwrap() - .push(Action::ProcessPair(log.address())); + let IUniswapV2Pair::Swap { + sender: _, + amount0In, + amount1In, + amount0Out, + amount1Out, + to: _, + } = log.log_decode()?.inner.data; + + let pair_address = log.address(); + let pair_already_known = pairs.lock().unwrap().get(pair_address).is_some(); + + debug!("Event by pair {:?}", pair_address); + + if pair_already_known { + tx.send(Action::ProcessOldPair( + pair_address, + amount0In, + amount1In, + amount0Out, + amount1Out, + )) + .await?; + } else { + tx.send(Action::ProcessNewPair(pair_address)).await?; + } } Ok(()) } -async fn process_pair( - ws: WsConnect, +async fn process_new_pair( pairs: Arc>, pair_address: Address, + provider: Arc, ) -> eyre::Result<()> { - let provider = ProviderBuilder::new().on_ws(ws).await.unwrap(); + let result: eyre::Result<()> = async { + let pair = IUniswapV2Pair::new(pair_address, provider.clone()); // todo can avoid che clone? + 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; - 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(()) + } + .await; + + if let Err(e) = &result { + eprintln!("error adding the new pair {}: {}", pair_address, e); + } + + result +} + +fn process_old_pair( + pairs: Arc>, + pair_address: Address, + amount0_in: U256, + amount1_in: U256, + amount0_out: U256, + amount1_out: U256, +) -> eyre::Result<()> { + let (reserve0, reserve1) = pairs.lock().unwrap().get_reserves(pair_address).unwrap(); pairs .lock() .unwrap() - .add(pair_address, token0, token1, reserve0, reserve1, factory); + .update_reserves( + pair_address, + reserve0 - U112::from(amount0_out) + U112::from(amount0_in), + reserve1 - U112::from(amount1_in) + U112::from(amount1_out), + ) + .unwrap(); // TODO manage error + Ok(()) // TODO manage errors +} + +async fn process_known_pairs( + pairs: Arc>, + provider: Arc, +) -> eyre::Result<()> { + let addresses = pairs.lock().unwrap().get_addresses(); + let len = addresses.len(); + + info!("Recovering state of {:?} saved pairs", len); + + for (i, address) in addresses.into_iter().enumerate() { + info!("Processing pair {}/{}: {:?}", i + 1, len, address); + + let result: eyre::Result<()> = async { + let pair = IUniswapV2Pair::new(address, provider.clone()); + let reserves = pair.getReserves().call().await?; + let reserve0 = reserves.reserve0; + let reserve1 = reserves.reserve1; + + let _ = pairs + .lock() + .unwrap() + .update_reserves(address, reserve0, reserve1); // TODO manage error, should be ok however + + Ok(()) + } + .await; + + if let Err(e) = &result { + eprintln!("Error processing pair {}: {}", address, e); + return result; + } + } Ok(()) } -async fn consume_priority_queue( - ws: WsConnect, - pairs: Arc>, - priority_queue: Arc>, - config: Config, -) { - let mut guard = priority_queue.lock().unwrap(); - let actions: Vec = guard.0.drain(..).collect(); //move all actions to temporary vector in order to unlock the mutex - drop(guard); //release before the expensive operation +fn look_for_opportunity(pairs: Arc>, involved_pairs: &HashSet
) { + let pairs = pairs.lock().unwrap(); - 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; + for pair_address in involved_pairs { + let (token0, token1) = pairs.get_tokens(*pair_address).unwrap(); + let _opportunities = pairs.look_for_opportunity(token0, token1); + } } async fn manage_interruption(pairs: Arc>, config: Config) -> eyre::Result<()> { @@ -126,25 +206,93 @@ async fn manage_interruption(pairs: Arc>, config: Config) -> eyre:: std::process::exit(0); } -pub fn run(config: Config) -> Result<()> { - let runtime = tokio::runtime::Runtime::new().unwrap(); +pub fn run(config: Config) -> miette::Result<()> { + let runtime = tokio::runtime::Runtime::new().map_err(|e| miette!(e))?; 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); + let (tx, mut rx) = mpsc::channel::(5000); + 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())); + let retry_layer = RetryBackoffLayer::new(50, 500, 100); + let client = RpcClient::builder() + .layer(retry_layer) + .ws(ws) + .await + .map_err(|e| miette!(e))?; + let provider = Arc::new(ProviderBuilder::new().on_client(client)); + let signer: PrivateKeySigner = "".parse().unwrap(); + + info!("Subscribing to the events..."); + tokio::spawn(subscribe(provider.clone(), pairs.clone(), tx.clone())); + + info!("Processing known pairs..."); + process_known_pairs(pairs.clone(), provider.clone()) + .await + .map_err(|e| miette!(e))?; + info!("Finished processing known pairs..."); + + let mut queue_last_time_not_empty = Instant::now(); + let mut block_processed = false; + let mut involved_pairs: HashSet
= HashSet::new(); loop { - consume_priority_queue(ws.clone(), pairs.clone(), priority_queue.clone(), config.clone()).await; + let action = rx.try_recv(); - debug!("The entire queue has been processed, waiting 100ms before checking if new actions are available..."); - sleep(Duration::from_millis(100)).await; + if let Ok(action) = action { + queue_last_time_not_empty = Instant::now(); + block_processed = false; + let len = rx.len(); + + debug!( + "Processing action {:?}, {:?} actions left", + action, len + ); + + match action { + Action::ProcessNewPair(pair_address) => { + process_new_pair(pairs.clone(), pair_address, provider.clone()) + .await + .map_err(|e| miette!(e))?; + involved_pairs.insert(pair_address); + } + Action::ProcessOldPair( + pair_address, + amount0_in, + amount1_in, + amount0_out, + amount1_out, + ) => { + process_old_pair( + pairs.clone(), + pair_address, + amount0_in, + amount1_in, + amount0_out, + amount1_out, + ) + .map_err(|e| miette!(e))?; + involved_pairs.insert(pair_address); + } + } + } else { + if !block_processed && Instant::now().duration_since(queue_last_time_not_empty) > Duration::from_millis(50) { + info!("The actions queue has been empty for 100ms, we assume the entire block has been processed"); + info!("Involved pairs: {:?}", involved_pairs); + + look_for_opportunity(pairs.clone(), &involved_pairs); + + block_processed = true; + involved_pairs.clear(); + }; + + std::thread::sleep(Duration::from_millis(10)); + } } - }); - Ok(()) + #[allow(unreachable_code)] + Ok(()) + }) } diff --git a/onchain/src/ArbitrageManager.sol b/onchain/src/ArbitrageManager.sol index adafc7f..85aec93 100644 --- a/onchain/src/ArbitrageManager.sol +++ b/onchain/src/ArbitrageManager.sol @@ -3,8 +3,10 @@ pragma solidity ^0.8.28; import {IUniswapV2Pair} from "./IUniswapV2Pair.sol"; import {IERC20} from "./IERC20.sol"; +import {IUniswapV2Callee} from "./IUniswapV2Callee.sol"; -contract ArbitrageManager { + +contract ArbitrageManager is IUniswapV2Callee { uint256 constant f = 997; function sqrt(uint256 x) @@ -54,36 +56,59 @@ contract ArbitrageManager { 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 phi = sqrt(f ** 2 * X_A * Y_B * X_B * Y_A / 1000**2); 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) + function flashArbitrage(address firstPair, address secondPair, bool tokenDir) + public + returns (uint256 gain) { - IUniswapV2Pair pairA = IUniswapV2Pair(_pairA); - IUniswapV2Pair pairB = IUniswapV2Pair(_pairB); + IUniswapV2Pair pairA = IUniswapV2Pair(firstPair); + IUniswapV2Pair pairB = IUniswapV2Pair(secondPair); - IERC20 tokenA = direction ? IERC20(pairA.token0()) : IERC20(pairA.token1()); + IERC20 firstToken = tokenDir ? IERC20(pairA.token0()) : IERC20(pairA.token1()); + IERC20 secondToken = tokenDir ? IERC20(pairA.token1()) : IERC20(pairA.token0()); + + (uint256 X_A, uint256 Y_A,) = pairA.getReserves(); + (uint256 X_B, uint256 Y_B,) = pairB.getReserves(); - // Transfer the input tokens from the sender to pairA - tokenA.transferFrom(msg.sender, address(pairA), amountIn); + uint256 amountIn = optimalIn(tokenDir ? X_B : X_A, tokenDir ? Y_B : Y_A, tokenDir ? X_A : X_B, tokenDir ? Y_A : Y_B); + uint256 firstAmountOut = getAmountOut(amountIn, tokenDir ? X_A : Y_A, tokenDir ? Y_A : X_A); + uint256 secondAmountOut = getAmountOut(firstAmountOut, tokenDir ? Y_B : X_B, tokenDir ? X_B : Y_B); - // 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)); + require(secondAmountOut > amountIn, "Not profitable"); - // 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)); + bytes memory data = abi.encode(pairA, pairB, firstToken, secondToken, amountIn, secondAmountOut); + pairA.swap(tokenDir ? 0 : firstAmountOut, tokenDir ? firstAmountOut : 0, address(this), data); + uint256 profit = secondAmountOut - amountIn; + firstToken.transfer(msg.sender, profit); + + return profit; + } - // Ensure that the arbitrage is profitable - require(amountOut > amountIn, "Arbitrage not profitable"); + function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes memory data) + public + { + (address pairA, address pairB, address firstToken, address secondToken, uint256 amountIn, uint256 secondAmountOut) = abi.decode(data, (address, address, address, address, uint256, uint256)); + + bool tokenDir = amount0 == 0; + IERC20(secondToken).transfer(pairB, tokenDir ? amount1 : amount0); + IUniswapV2Pair(pairB).swap(tokenDir ? secondAmountOut : 0, tokenDir ? 0 : secondAmountOut, sender, new bytes(0)); + IERC20(firstToken).transfer(pairA, amountIn); + } + + fallback() external { + ( + address sender, + uint256 amount0, + uint256 amount1, + bytes memory data + ) = abi.decode(msg.data[4:], (address, uint256, uint256, bytes)); + + uniswapV2Call(sender, amount0, amount1, data); } } diff --git a/onchain/src/IUniswapV2Callee.sol b/onchain/src/IUniswapV2Callee.sol new file mode 100644 index 0000000..b21067d --- /dev/null +++ b/onchain/src/IUniswapV2Callee.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.28; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} diff --git a/onchain/test/ArbitrageManager.t.sol b/onchain/test/ArbitrageManager.t.sol index 87268f3..b03cddb 100644 --- a/onchain/test/ArbitrageManager.t.sol +++ b/onchain/test/ArbitrageManager.t.sol @@ -16,7 +16,6 @@ contract ArbitrageTest is Test { 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(); @@ -34,12 +33,13 @@ contract ArbitrageTest is Test { n = 115792089237316195423570985008687907853269984665640564039457584007913129639935; assertEq(340282366920938463463374607431768211456 - 1, arbitrageManager.sqrt(n)); // +-1 is an acceptable rounding error } - - function test_computeAmountIn() public { + + function test_swapUsingOptimum() 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("Sushiswap pair reserves", X_B, Y_B); console.log("Uniswap pair ratio", Y_A/X_A); console.log("Sushiswap pair ratio", Y_B/X_B); @@ -53,6 +53,7 @@ contract ArbitrageTest is Test { (X_A, Y_A, ) = uniswapPair.getReserves(); (X_B, Y_B, ) = sushiswapPair.getReserves(); console.log("Uniswap pair reserves", X_A, Y_A); + console.log("Sushiswap pair reserves", X_B, Y_B); console.log("Uniswap pair ratio", Y_A/X_A); console.log("Sushiswap pair ratio", Y_B/X_B); @@ -74,4 +75,47 @@ contract ArbitrageTest is Test { console.log("Uniswap pair ratio", Y_A/X_A); console.log("Sushiswap pair ratio", Y_B/X_B); } + + function computeGain(uint256 X_A, uint256 Y_A, uint256 X_B, uint256 Y_B, int256 delta) + internal view returns(uint256) + { + uint256 optimum = (delta > 0) ? + arbitrageManager.optimalIn(X_A, Y_A, X_B, Y_B) + uint256(delta) + : arbitrageManager.optimalIn(X_A, Y_A, X_B, Y_B) - uint256(-delta); + uint256 amountOut = arbitrageManager.getAmountOut(optimum, Y_A, X_A); + amountOut = arbitrageManager.getAmountOut(amountOut, X_B, Y_B); + return amountOut - optimum; + } + + function test_computeOptimum() public view { + (uint256 X_A, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH) + (uint256 X_B, uint256 Y_B, ) = sushiswapPair.getReserves(); // (USDT, WETH) + Y_A -= Y_A / 5; // unbalancing the pair + + // Using delta too low (~< 10**8) seems to produce a better gain, + // I *believe this has to do to some rounding, it should be neglibile + uint256[4] memory deltas = [uint256(0), uint256(10**8), uint256(10**9), uint256(10**10)]; + + uint256 gain = computeGain(X_A, Y_A, X_B, Y_B, 0); + + for (uint256 i; i < deltas.length; i++) { + assertGe(gain, computeGain(X_A, Y_A, X_B, Y_B, int256(deltas[i])), "Computed optimum isnt't really optimal"); + assertGe(gain, computeGain(X_A, Y_A, X_B, Y_B, -int256(deltas[i])), "Computed optimum isnt't really optimal"); + } + } + + function test_flashArbitrage () public { + uint256 initialWethBalance = weth.balanceOf(address(this)); + + console.log("initial weth balance", initialWethBalance); + (, uint256 Y_A, ) = uniswapPair.getReserves(); // (USDT, WETH) + uint256 unbalance = Y_A / 5; + vm.prank(address(uniswapPair)); // it works only for the next call + weth.transfer(address(0), unbalance); + uniswapPair.sync(); + + uint256 profit = arbitrageManager.flashArbitrage(address(uniswapPair), address(sushiswapPair), false); + console.log("profit", profit); + assertEq(initialWethBalance + profit, weth.balanceOf(address(this)), "There was no profit"); + } } diff --git a/secrets/alchemy_key.age b/secrets/alchemy_key.age new file mode 100644 index 0000000..cf74662 --- /dev/null +++ b/secrets/alchemy_key.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 Zh7Kmw 6NFxuvVROgzHIvJPZqniuXinr9XMhNtt4hwW0do9Gio +g8FOQSOHN0xF7QV1fa9lkq62Fim+TQaWWLqGjppn2QE +--- /bcjNPkDej+yknSFozObJz/QAY4fzzVOm6V4iE5BBHc +¿4¬î| ¶>Þ,K8’ö¢hb< 'U¾œq× Zµb¬zEÂùÀ*OPSÏKôŸ“PqµqkÇ tpŸ¤zcë \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..8a2e8ec --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,7 @@ +let + aciceri = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIm9Sl/I+5G4g4f6iE4oCUJteP58v+wMIew9ZuLB+Gea"; +in +{ + "alchemy_key.age".publicKeys = [ aciceri ]; + "wallet_private_key.age".publicKeys = [ aciceri ]; +} diff --git a/secrets/wallet_private_key.age b/secrets/wallet_private_key.age new file mode 100644 index 0000000..e4ed16e --- /dev/null +++ b/secrets/wallet_private_key.age @@ -0,0 +1,12 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFpoN0ttdyB0VC96 +SmZNQjJpMlR3eXU2bzNmK1BYMk5ta3JpSDFCZGpmS2k4R3B6L1NFClJZS055amRO +UEJXL1IvUjN3Mnppbks5emxaUlpoRkhLUEZVRUhwKzFpRUkKLT4gfkwoQUlUJlot +Z3JlYXNlIHojVEdeIF1VSmlVIFxfYApnd1o4SitNK1NKR0dMaDBEUUk4QndKY3hB +YTFTUGtsL0JRWVIzM1lzbmhUUlpxdSs0d3RMd2NQU3Y2ZG9MdHNMCk1rcFFvYzBX +dnVmMjcrcnBFbHdVb0pNbjlObnNtRkx4ZDNYZkRSWWN3dnF3UkxIQ1ptSmJjSGN4 +d1BKZgotLS0gdHlBMHpGeGdqdElFUWJZVWVoc0x4MGEvc3lGMzhkUGFHTHlCbkNy +c2JIawp8pD+QIU4hfw8ySNWye098z1ZQSXn267JuzH1oE20GY0ubK2TDWfxUHNht +jBhdgTnVPqQmBX8N0wDeB16AWmC/YuPEz52zZzgZ85Hy61N7E9m5ZDOaBhb1VJpD +Pf9T0uo= +-----END AGE ENCRYPTED FILE-----