diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8bce90..6da8e5b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ name: Continuous Integration on: - pull_request: + pull_request_target: permissions: id-token: write diff --git a/.github/workflows/evm-foundry-ci.yml b/.github/workflows/evm-foundry-ci.yml index ee56fa0..67647d7 100644 --- a/.github/workflows/evm-foundry-ci.yml +++ b/.github/workflows/evm-foundry-ci.yml @@ -4,7 +4,7 @@ on: push: branches: - main - pull_request: + pull_request_target: jobs: check: @@ -13,12 +13,13 @@ jobs: env: RPC_URL: ${{ secrets.ETH_RPC_URL }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: + ref: ${{ github.event.pull_request.head.sha }} submodules: recursive - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c - name: Check formatting run: forge fmt --check diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..9e0ce55 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,30 @@ +name: Release + +on: + workflow_dispatch: + inputs: { } + release: + types: [ created, prereleased ] + +permissions: + id-token: write + contents: write + +jobs: + publish-crate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Push to crates.io + run: | + cargo publish --locked --verbose --token ${{ secrets.CRATESIO_REGISTRY_TOKEN }} +# we can't use the action because it errors on github dependencies in any workspace crate +# - uses: katyo/publish-crates@v2 +# with: +# path: "./tycho-common" +# registry-token: ${{ secrets.CRATESIO_REGISTRY_TOKEN }} + diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 349f500..8fdc1ee 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -10,15 +10,11 @@ jobs: analyze: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - name: Build the contracts - run: forge build --build-info + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.event.pull_request.head.sha }} + submodules: recursive + - uses: crytic/slither-action@f197989dea5b53e986d0f88c60a034ddd77ec9a8 with: target: 'foundry/' - - uses: crytic/slither-action@v0.4.0 - with: - target: 'foundry/' - ignore-compile: true - slither-args: '--filter-paths foundry/lib/' \ No newline at end of file + slither-args: '--filter-paths foundry/lib/' diff --git a/.github/workflows/tests-and-lints-template.yaml b/.github/workflows/tests-and-lints-template.yaml index 70f431b..2c9ffcc 100644 --- a/.github/workflows/tests-and-lints-template.yaml +++ b/.github/workflows/tests-and-lints-template.yaml @@ -39,7 +39,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: getsentry/action-github-app-token@v2 + uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc with: app_id: ${{ secrets.app_id }} private_key: ${{ secrets.app_private_key }} @@ -48,7 +48,9 @@ jobs: run: sudo apt update && sudo apt install -y git - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup git to use https run: | @@ -57,17 +59,17 @@ jobs: git config --global url."https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com".insteadOf ssh://github.com - name: Setup toolchain - uses: dtolnay/rust-toolchain@v1 + uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 with: toolchain: ${{ matrix.toolchain }} - name: Setup Rust Cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 with: cache-on-failure: true - name: Install latest nextest release - uses: taiki-e/install-action@nextest + uses: taiki-e/install-action@b239071b2aedda3db20530301c2d88cd538e90d3 - name: Test run: cargo nextest run --workspace --lib --all-targets && cargo test --doc @@ -79,7 +81,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: getsentry/action-github-app-token@v2 + uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc with: app_id: ${{ secrets.app_id }} private_key: ${{ secrets.app_private_key }} @@ -88,7 +90,9 @@ jobs: run: sudo apt update && sudo apt install -y git - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup git to use https run: | @@ -97,13 +101,13 @@ jobs: git config --global url."https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com".insteadOf ssh://github.com - name: Setup clippy toolchain - stable - uses: dtolnay/rust-toolchain@v1 + uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 with: toolchain: stable components: clippy - name: Setup Rust Cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 with: cache-on-failure: true @@ -116,7 +120,7 @@ jobs: RUSTFLAGS: -Dwarnings - name: Setup rustfmt toolchain - nightly - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 with: components: rustfmt diff --git a/.gitmodules b/.gitmodules index eacb977..d9919b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "foundry/lib/v4-periphery"] path = foundry/lib/v4-periphery url = https://github.com/Uniswap/v4-periphery +[submodule "foundry/lib/solady"] + path = foundry/lib/solady + url = https://github.com/vectorized/solady diff --git a/CHANGELOG.md b/CHANGELOG.md index 3258cde..b4ffaa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,167 @@ +## [0.76.0](https://github.com/propeller-heads/tycho-execution/compare/0.75.1...0.76.0) (2025-04-03) + + +### Features + +* deploy Ekubo gas optimizations ([9012d7b](https://github.com/propeller-heads/tycho-execution/commit/9012d7b4d1745a3c5315d12c7c412d6e3267e4ba)) + +## [0.75.1](https://github.com/propeller-heads/tycho-execution/compare/0.75.0...0.75.1) (2025-04-03) + + +### Bug Fixes + +* Proper ekubo protocol name in GROUPABLE_PROTOCOLS ([b4c687b](https://github.com/propeller-heads/tycho-execution/commit/b4c687bc3f51167fc942b8b7b00b17df4a2bec30)) + +## [0.75.0](https://github.com/propeller-heads/tycho-execution/compare/0.74.0...0.75.0) (2025-04-03) + + +### Features + +* Unichain deployment ([d05e118](https://github.com/propeller-heads/tycho-execution/commit/d05e1183d4e85eb57c139b1fd5411833efea92fc)) + + +### Bug Fixes + +* Run foundry tests on PR branch (not main) ([43f1a07](https://github.com/propeller-heads/tycho-execution/commit/43f1a0701707d2dbf5bfa4f6ccde50aea48e46a1)) +* Set native and wrapped tokens for Unichain ([4878229](https://github.com/propeller-heads/tycho-execution/commit/4878229e1d60a317b0f2d97ea57886e64cc70cb5)) + +## [0.74.0](https://github.com/propeller-heads/tycho-execution/compare/0.73.0...0.74.0) (2025-04-02) + + +### Features + +* Fix rollFork usage for Ekubo test ([6cdca83](https://github.com/propeller-heads/tycho-execution/commit/6cdca8381e711bc01b7573dd25cafc9aa057aac3)) + + +### Bug Fixes + +* Fix tests after cherry picking ([5336969](https://github.com/propeller-heads/tycho-execution/commit/5336969df8b06238b65a9c9f1a3458f43b89cb54)) + +## [0.73.0](https://github.com/propeller-heads/tycho-execution/compare/0.72.0...0.73.0) (2025-04-02) + + +### Features + +* Add router_address to cli ([1f6f1a4](https://github.com/propeller-heads/tycho-execution/commit/1f6f1a4236d577e57f50d063a81e9a1ed801a6dd)) + +## [0.72.0](https://github.com/propeller-heads/tycho-execution/compare/0.71.0...0.72.0) (2025-04-02) + + +### Features + +* Make EncodingContext.router_address optional ([8865e22](https://github.com/propeller-heads/tycho-execution/commit/8865e22116dcb8c291caa745de78b4e6241315c8)) +* Remove router_address from Solution, set default ([d5c589d](https://github.com/propeller-heads/tycho-execution/commit/d5c589d2c09da8e7f22b40be6e5b236e0eb16645)) +* Support manual router address setting in builder ([c336a28](https://github.com/propeller-heads/tycho-execution/commit/c336a28905a1829da78997ea2126849fdabbcfc6)) + + +### Bug Fixes + +* fix Solution.router_address for Ekubo ([b397ddd](https://github.com/propeller-heads/tycho-execution/commit/b397ddd2beb007d0bed378949d35a9ce5c5b76c9)) + +## [0.71.0](https://github.com/propeller-heads/tycho-execution/compare/0.70.0...0.71.0) (2025-04-01) + + +### Features + +* Update ekubo router address ([e3d25fc](https://github.com/propeller-heads/tycho-execution/commit/e3d25fcd5ed4e160fff10d5c346b12f0cb3328c6)) + + +### Bug Fixes + +* update ekubo_v2 executor ([57aa1c3](https://github.com/propeller-heads/tycho-execution/commit/57aa1c3402da976cfed38db2b46c584162cdbb69)) + +## [0.70.0](https://github.com/propeller-heads/tycho-execution/compare/0.69.0...0.70.0) (2025-03-31) + + +### Features + +* Support Ekubo callback in TychoRouter ([b3078f9](https://github.com/propeller-heads/tycho-execution/commit/b3078f9c7b99b3c0c9f0008b97855b48483f06dc)) + + +### Bug Fixes + +* ekubo -> ekubo_v2 ([18fa0cc](https://github.com/propeller-heads/tycho-execution/commit/18fa0cc7adfaa46879aa3637c03563c770582b0d)) +* ekubo -> ekubo_v2 ([6c35f11](https://github.com/propeller-heads/tycho-execution/commit/6c35f114e383b7ba93cb44d314a05f69ab15fadd)) +* Finalize ekubo executor address ([c0068d4](https://github.com/propeller-heads/tycho-execution/commit/c0068d456bbd271d1c74797577e0fe514be0fcc7)) +* support payCallback method for Ekubo ([7551612](https://github.com/propeller-heads/tycho-execution/commit/75516122e1a084f86c34e6eaaf43fe5f53a30d96)) +* Take address for EkuboExecutor init ([c678f40](https://github.com/propeller-heads/tycho-execution/commit/c678f400571d1b001c98595f68d9a99e0cf4900d)) +* test setup fix after rebase ([28f9f24](https://github.com/propeller-heads/tycho-execution/commit/28f9f244e6343393a96020a85f5e202012d7ca26)) + +## [0.69.0](https://github.com/propeller-heads/tycho-execution/compare/0.68.2...0.69.0) (2025-03-31) + + +### Features + +* Add PancakeSwapV3 support to encoding ([fa024a4](https://github.com/propeller-heads/tycho-execution/commit/fa024a4a6702c7809af31a6ba392338d9368c6d2)) +* Pancakeswap V3 support ([d582543](https://github.com/propeller-heads/tycho-execution/commit/d582543057665b737cc0aab5243ccc22db1f0a13)) + +## [0.68.2](https://github.com/propeller-heads/tycho-execution/compare/0.68.1...0.68.2) (2025-03-28) + + +### Bug Fixes + +* fix for foundry tests external contributors ([a9ddb0e](https://github.com/propeller-heads/tycho-execution/commit/a9ddb0e6e9ef546f6e851c3056df5a55ee4dfa76)) + +## [0.68.1](https://github.com/propeller-heads/tycho-execution/compare/0.68.0...0.68.1) (2025-03-27) + + +### Bug Fixes + +* Add crate metadata ([7e7fabf](https://github.com/propeller-heads/tycho-execution/commit/7e7fabf51bff842ab20c2f512e4f3a609a266e79)) + +## [0.68.0](https://github.com/propeller-heads/tycho-execution/compare/0.67.2...0.68.0) (2025-03-27) + + +### Features + +* switch to tycho_commons ([0836bf7](https://github.com/propeller-heads/tycho-execution/commit/0836bf7d530f18a6c0f112542bcad16050e88afa)) + + +### Bug Fixes + +* Handle unichain chain id ([379858b](https://github.com/propeller-heads/tycho-execution/commit/379858bfca27eb5e8180a32351337779e625e0b5)) + +## [0.67.2](https://github.com/propeller-heads/tycho-execution/compare/0.67.1...0.67.2) (2025-03-27) + + +### Bug Fixes + +* prepared lint workflow for external contributors ([9896f48](https://github.com/propeller-heads/tycho-execution/commit/9896f4882940517d61852300420c7c580138406f)) +* prepared lint workflow for external contributors ([5162b9e](https://github.com/propeller-heads/tycho-execution/commit/5162b9e19efcaa5a2137f71a94f6f9e7f7d14da0)) + +## [0.67.1](https://github.com/propeller-heads/tycho-execution/compare/0.67.0...0.67.1) (2025-03-27) + + +### Bug Fixes + +* added empty line ([b3c4dbc](https://github.com/propeller-heads/tycho-execution/commit/b3c4dbc293df758ff4cff949298a819436d83c38)) +* fixed git checkout for codelint ([58e2ddd](https://github.com/propeller-heads/tycho-execution/commit/58e2ddd50e131c484ad53a6dca0b09e1d221d0e5)) +* prepared lint workflow for external contributors ([9f7d605](https://github.com/propeller-heads/tycho-execution/commit/9f7d605ea5e76d230b5946c618ece76365fb4f02)) +* removed empty line ([ae5d7de](https://github.com/propeller-heads/tycho-execution/commit/ae5d7deaccfc1ac527f88371bb3f055b01689801)) +* test run outside a PR ([af01972](https://github.com/propeller-heads/tycho-execution/commit/af0197205adb3220673022b690f1d8aa6f6734aa)) + +## [0.67.0](https://github.com/propeller-heads/tycho-execution/compare/0.66.1...0.67.0) (2025-03-20) + + +### Features + +* Set v2/v3 executor addresses on ethereum ([783712b](https://github.com/propeller-heads/tycho-execution/commit/783712be5d8dae626c735193416ba03701d3a616)) +* Support Pancakeswap v3 on ethereum ([2a4ee88](https://github.com/propeller-heads/tycho-execution/commit/2a4ee88cad46dfeb068809bdd885e63094020bcd)) +* Support sushiswap v2 and pancakeswap v2 on ethereum ([0a8a34b](https://github.com/propeller-heads/tycho-execution/commit/0a8a34be035588d45e6b72a42f4dd691e3c98d2f)) + + +### Bug Fixes + +* proper exchange name when deploying executors ([39bd9df](https://github.com/propeller-heads/tycho-execution/commit/39bd9df4b6ca8db78bb4d6757c93b30cce29f360)) +* Remove pancakeswap V3 from approved executor addresses ([1ed149a](https://github.com/propeller-heads/tycho-execution/commit/1ed149a9b8ffd41b9cc702c77e146fa234176af0)) + +## [0.66.1](https://github.com/propeller-heads/tycho-execution/compare/0.66.0...0.66.1) (2025-03-19) + + +### Bug Fixes + +* Slippage precision calculation ([d644b63](https://github.com/propeller-heads/tycho-execution/commit/d644b63851a63babadfb909af97c5bf80dd03376)) + ## [0.66.0](https://github.com/propeller-heads/tycho-execution/compare/0.65.1...0.66.0) (2025-03-14) diff --git a/Cargo.lock b/Cargo.lock index 6bf10b6..7d69956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4316,9 +4316,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tycho-core" -version = "0.61.1" -source = "git+https://github.com/propeller-heads/tycho-indexer.git?tag=0.61.1#aae2c11bffe9ae5e436adc019b769438fa038272" +name = "tycho-common" +version = "0.64.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e318a43fab79199deaab2391c83c75724780151c0337b67914ed835ff04b52f" dependencies = [ "anyhow", "async-trait", @@ -4340,7 +4341,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.66.0" +version = "0.76.0" dependencies = [ "alloy", "alloy-primitives", @@ -4358,7 +4359,7 @@ dependencies = [ "serde_json", "thiserror 1.0.69", "tokio", - "tycho-core", + "tycho-common", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6d58787..3979f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,15 @@ [package] name = "tycho-execution" -version = "0.66.0" +version = "0.76.0" edition = "2021" +description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." +repository = "https://github.com/propeller-heads/tycho-execution" +homepage = "https://www.propellerheads.xyz/tycho" +documentation = "https://docs.propellerheads.xyz/tycho" +keywords = ["propellerheads", "solver", "defi", "dex", "mev"] +license = "MIT" +categories = ["finance", "cryptography::cryptocurrencies"] +readme = "README.md" [[bin]] name = "tycho-encode" @@ -24,7 +32,7 @@ clap = { version = "4.5.3", features = ["derive"] } alloy = { version = "0.9.2", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true } alloy-sol-types = { version = "0.8.14", optional = true } alloy-primitives = { version = "0.8.9", optional = true } -tycho-core = { git = "https://github.com/propeller-heads/tycho-indexer.git", package = "tycho-core", tag = "0.61.1" } +tycho-common = "0.64.1" once_cell = "1.20.2" [dev-dependencies] diff --git a/config/executor_addresses.json b/config/executor_addresses.json index 02e98e6..77d684b 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -1,9 +1,13 @@ { "ethereum": { "uniswap_v2": "0xf6c5be66FFf9DC69962d73da0A617a827c382329", + "sushiswap_v2": "0x8ABd4075cF54E0A9C52D18e6951b969AF3249cF9", + "pancakeswap_v2": "0x9fC9e63cCf5F773A8bC79DcfA38c581B0DEa1d11", "uniswap_v3": "0xdD8559c917393FC8DD2b4dD289c52Ff445fDE1B0", + "pancakeswap_v3": "0x4929B619A8F0D9c06ed0FfD497636580D823F65d", "uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70", - "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91" + "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91", + "ekubo_v2": "0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D" }, "tenderly_ethereum": { "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", @@ -13,10 +17,17 @@ }, "base": { "uniswap_v2": "0x852389F3Cde47e7dab5a29Fdd3E58c7050f1a019", + "sushiswap_v2": "0xbAf4569234712233Ae2142A07Bf164f988bDf5B5", + "pancakeswap_v2": "0xd3d73f114CbB40AD17CEEfC046FB18ecDe08678D", "uniswap_v3": "0x447135F701A4650e4Bd19c2B2B99cb324f8942c3", "uniswap_v4": "0xc34689d354B42b2780Fbe0A785080d761E5C882b" }, "tenderly_base": { "uniswap_v3": "0x7c7E06d7317e620a185078e236879D2a87fC8d22" + }, + "unichain": { + "uniswap_v2": "0x2E80FBD880379744b46C2aB7249D2949E6696353", + "uniswap_v3": "0x7ba9149EaCC5801279FF8B4d7ECa020bbef18F90", + "uniswap_v4": "0x475EB503D630C2919DC8B24cB6603E17e69Bfe35" } } diff --git a/config/router_addresses.json b/config/router_addresses.json new file mode 100644 index 0000000..95ba8e0 --- /dev/null +++ b/config/router_addresses.json @@ -0,0 +1,5 @@ +{ + "ethereum": "0xabA2fC41e2dB95E77C6799D0F580034395FF2B9E", + "base": "0xC2C23b0199525DE070D126860133dc3badaD2EEb", + "unichain": "0x9BDC3bE75440dBE563527CB39BB11cFBD1E21b09" +} diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json new file mode 100644 index 0000000..4bc8412 --- /dev/null +++ b/config/test_executor_addresses.json @@ -0,0 +1,12 @@ +{ + "ethereum": { + "uniswap_v2": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f", + "sushiswap_v2": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f", + "pancakeswap_v2": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f", + "uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", + "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", + "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", + "ekubo_v2": "0x2a07706473244BC757E10F2a9E86fB532828afe3" + } +} diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 48ee12b..7ccc0bc 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use num_bigint::BigUint; -use tycho_core::{ +use tycho_common::{ models::{protocol::ProtocolComponent, Chain}, Bytes, }; @@ -13,8 +13,6 @@ use tycho_execution::encoding::{ fn main() { // Setup variables - let router_address = Bytes::from_str("0x1234567890abcdef1234567890abcdef12345678") - .expect("Failed to create router address"); let swapper_pk = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); let user_address = Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") @@ -61,7 +59,6 @@ fn main() { exact_out: false, // it's an exact in solution checked_amount: None, // the amount out will not be checked in execution swaps: vec![simple_swap], - router_address, ..Default::default() }; diff --git a/foundry/hardhat.config.js b/foundry/hardhat.config.js index f1395ae..0603641 100644 --- a/foundry/hardhat.config.js +++ b/foundry/hardhat.config.js @@ -35,6 +35,11 @@ module.exports = { url: process.env.RPC_URL, accounts: [process.env.PRIVATE_KEY], chainId: 8453 + }, + unichain: { + url: process.env.RPC_URL, + accounts: [process.env.PRIVATE_KEY], + chainId: 130 } }, @@ -46,5 +51,15 @@ module.exports = { etherscan: { apiKey: process.env.BLOCKCHAIN_EXPLORER_API_KEY, + customChains: [ + { + network: "unichain", + chainId: 130, + urls: { + apiURL: "https://api.uniscan.xyz/api", + browserURL: "https://www.uniscan.xyz/" + } + } + ] } }; diff --git a/foundry/lib/ekubo/interfaces/ICore.sol b/foundry/lib/ekubo/interfaces/ICore.sol new file mode 100644 index 0000000..d2388e8 --- /dev/null +++ b/foundry/lib/ekubo/interfaces/ICore.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {IFlashAccountant} from "./IFlashAccountant.sol"; +import {EkuboPoolKey} from "../types/poolKey.sol"; +import {SqrtRatio} from "../types/sqrtRatio.sol"; + +interface ICore is IFlashAccountant { + function swap_611415377( + EkuboPoolKey memory poolKey, + int128 amount, + bool isToken1, + SqrtRatio sqrtRatioLimit, + uint256 skipAhead + ) external payable returns (int128 delta0, int128 delta1); +} \ No newline at end of file diff --git a/foundry/lib/ekubo/interfaces/IFlashAccountant.sol b/foundry/lib/ekubo/interfaces/IFlashAccountant.sol new file mode 100644 index 0000000..2524078 --- /dev/null +++ b/foundry/lib/ekubo/interfaces/IFlashAccountant.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +interface ILocker { + function locked(uint256 id) external; +} + +interface IPayer { + function payCallback(uint256 id, address token) external; +} + +interface IFlashAccountant { + // Withdraws a token amount from the accountant to the given recipient. + // The contract must be locked, as it tracks the withdrawn amount against the current locker's delta. + function withdraw(address token, address recipient, uint128 amount) external; +} diff --git a/foundry/lib/ekubo/math/constants.sol b/foundry/lib/ekubo/math/constants.sol new file mode 100644 index 0000000..cd08e3b --- /dev/null +++ b/foundry/lib/ekubo/math/constants.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +// We use this address to represent the native token within the protocol +address constant NATIVE_TOKEN_ADDRESS = address(0); diff --git a/foundry/lib/ekubo/types/poolKey.sol b/foundry/lib/ekubo/types/poolKey.sol new file mode 100644 index 0000000..ad20db8 --- /dev/null +++ b/foundry/lib/ekubo/types/poolKey.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +// address (20 bytes) | fee (8 bytes) | tickSpacing (4 bytes) +type Config is bytes32; + +// Each pool has its own state associated with this key +struct EkuboPoolKey { + address token0; + address token1; + Config config; +} diff --git a/foundry/lib/ekubo/types/sqrtRatio.sol b/foundry/lib/ekubo/types/sqrtRatio.sol new file mode 100644 index 0000000..27e5856 --- /dev/null +++ b/foundry/lib/ekubo/types/sqrtRatio.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +type SqrtRatio is uint96; + +uint96 constant MIN_SQRT_RATIO_RAW = 4611797791050542631; +SqrtRatio constant MIN_SQRT_RATIO = SqrtRatio.wrap(MIN_SQRT_RATIO_RAW); +uint96 constant MAX_SQRT_RATIO_RAW = 79227682466138141934206691491; +SqrtRatio constant MAX_SQRT_RATIO = SqrtRatio.wrap(MAX_SQRT_RATIO_RAW); diff --git a/foundry/lib/solady b/foundry/lib/solady new file mode 160000 index 0000000..c9e079c --- /dev/null +++ b/foundry/lib/solady @@ -0,0 +1 @@ +Subproject commit c9e079c0ca836dcc52777a1fa7227ef28e3537b3 diff --git a/foundry/lib/v3-updated/CallbackValidationV2.sol b/foundry/lib/v3-updated/CallbackValidationV2.sol deleted file mode 100644 index a3c7a7f..0000000 --- a/foundry/lib/v3-updated/CallbackValidationV2.sol +++ /dev/null @@ -1,41 +0,0 @@ -// Updated v3 lib to solidity >=0.7.6 - -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.6; - -import "./PoolAddressV2.sol"; -import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; - -/// @notice Provides validation for callbacks from Uniswap V3 Pools -library CallbackValidationV2 { - /// @notice Returns the address of a valid Uniswap V3 Pool - /// @param factory The contract address of the Uniswap V3 factory - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @return pool The V3 pool contract address - function verifyCallback( - address factory, - address tokenA, - address tokenB, - uint24 fee - ) internal view returns (IUniswapV3Pool pool) { - return - verifyCallback( - factory, - PoolAddressV2.getPoolKey(tokenA, tokenB, fee) - ); - } - - /// @notice Returns the address of a valid Uniswap V3 Pool - /// @param factory The contract address of the Uniswap V3 factory - /// @param poolKey The identifying key of the V3 pool - /// @return pool The V3 pool contract address - function verifyCallback( - address factory, - PoolAddressV2.PoolKey memory poolKey - ) internal view returns (IUniswapV3Pool pool) { - pool = IUniswapV3Pool(PoolAddressV2.computeAddress(factory, poolKey)); - require(msg.sender == address(pool), "CV"); - } -} diff --git a/foundry/lib/v3-updated/PoolAddressV2.sol b/foundry/lib/v3-updated/PoolAddressV2.sol deleted file mode 100644 index c63aae7..0000000 --- a/foundry/lib/v3-updated/PoolAddressV2.sol +++ /dev/null @@ -1,59 +0,0 @@ -// Updated v3 lib to solidity >=0.7.6 - -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee -library PoolAddressV2 { - bytes32 internal constant POOL_INIT_CODE_HASH = - 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; - - /// @notice The identifying key of the pool - struct PoolKey { - address token0; - address token1; - uint24 fee; - } - - /// @notice Returns PoolKey: the ordered tokens with the matched fee levels - /// @param tokenA The first token of a pool, unsorted - /// @param tokenB The second token of a pool, unsorted - /// @param fee The fee level of the pool - /// @return Poolkey The pool details with ordered token0 and token1 assignments - function getPoolKey(address tokenA, address tokenB, uint24 fee) - internal - pure - returns (PoolKey memory) - { - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); - } - - /// @notice Deterministically computes the pool address given the factory and PoolKey - /// @param factory The Uniswap V3 factory contract address - /// @param key The PoolKey - /// @return pool The contract address of the V3 pool - function computeAddress(address factory, PoolKey memory key) - internal - pure - returns (address pool) - { - require(key.token0 < key.token1); - pool = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - factory, - keccak256( - abi.encode(key.token0, key.token1, key.fee) - ), - POOL_INIT_CODE_HASH - ) - ) - ) - ) - ); - } -} diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 04240ad..d10f28f 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -7,4 +7,6 @@ @uniswap/v3-updated/=lib/v3-updated/ @uniswap/v3-core/=lib/v3-core/ @uniswap/v4-core/=lib/v4-core/ -@uniswap/v4-periphery/=lib/v4-periphery/ \ No newline at end of file +@uniswap/v4-periphery/=lib/v4-periphery/ +@solady=lib/solady/src/ +@ekubo=lib/ekubo/ diff --git a/foundry/scripts/README.md b/foundry/scripts/README.md index aba7fcb..aa93ee2 100644 --- a/foundry/scripts/README.md +++ b/foundry/scripts/README.md @@ -38,7 +38,7 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly 1. Deploy router: `npx hardhat run scripts/deploy-router.js --network NETWORK` 2. Define the accounts to grant roles to in `scripts/roles.json` -3. Export the router address to the environment variable `export ROUTER=` +3. Export the router address to the environment variable `export ROUTER_ADDRESS=` 4. Grant roles: `npx hardhat run scripts/set-roles.js --network NETWORK` 5. Set executors: `npx hardhat run scripts/set-executors.js --network NETWORK`. Make sure you change the DEPLOY_WALLET to the executor deployer wallet. If you need to deploy executors, follow the instructions below. @@ -47,4 +47,4 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly 1. In `scripts/deploy-executors.js` define the executors to be deployed 2. Deploy executors: `npx hardhat run scripts/deploy-executors.js --network NETWORK` -3. Fill in the executor addresses in `config/executors.json` +3. Fill in the executor addresses in `config/executor_addresses.json` diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index 00b28b7..fa32960 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -5,18 +5,83 @@ const hre = require("hardhat"); // Comment out the executors you don't want to deploy const executors_to_deploy = { "ethereum":[ - {exchange: "UniswapV2Executor", args: ["0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"]}, - {exchange: "UniswapV3Executor", args: ["0x1F98431c8aD98523631AE4a59f267346ea31F984"]}, + // USV2 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" + ]}, + // SUSHISWAP - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", + "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" + ]}, + // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x1097053Fd2ea711dad45caCcc45EfF7548fCB362", + "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" + ]}, + // USV3 -Args: Factory, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ]}, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ]}, + // Args: Pool manager {exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]}, {exchange: "BalancerV2Executor", args: []}, + // Args: Ekubo core contract + {exchange: "EkuboExecutor", args: [ + "0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444" + ]} ], "base":[ - {exchange: "UniswapV2Executor", args: ["0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6"]}, - {exchange: "UniswapV3Executor", args: ["0x33128a8fC17869897dcE68Ed026d694621f6FDfD"]}, + // Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" + ]}, + // SUSHISWAP V2 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x71524B4f93c58fcbF659783284E38825f0622859", + "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" + ]}, + // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", + "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" + ]}, + // USV3 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ]}, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ]}, + // Args: Pool manager {exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]}, {exchange: "BalancerV2Executor", args: []}, ], - + "unichain":[ + // Args: Factory, Pool Init Code Hash + {exchange: "UniswapV2Executor", args: [ + "0x1f98400000000000000000000000000000000002", + "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" + ]}, + // USV3 - Args: Factory, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x1f98400000000000000000000000000000000003", + "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" + ]}, + // Args: Pool manager + {exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]}, + ], } async function main() { diff --git a/foundry/scripts/deploy-router.js b/foundry/scripts/deploy-router.js index 87e0f15..103fd0d 100644 --- a/foundry/scripts/deploy-router.js +++ b/foundry/scripts/deploy-router.js @@ -13,6 +13,10 @@ async function main() { // permit2 address is the same as on ethereum permit2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; weth = "0x4200000000000000000000000000000000000006"; + } else if (network === "unichain") { + // permit2 address is the same as on ethereum + permit2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + weth = "0x4200000000000000000000000000000000000006"; } else { throw new Error(`Unsupported network: ${network}`); } diff --git a/foundry/scripts/roles.json b/foundry/scripts/roles.json index 3a7ad7a..2d477fe 100644 --- a/foundry/scripts/roles.json +++ b/foundry/scripts/roles.json @@ -58,5 +58,20 @@ "FUND_RESCUER_ROLE": [ "0xb0A77f867Fcec1e9b271Ee17354bC6bBC0dD5662" ] + }, + "unichain": { + "EXECUTOR_SETTER_ROLE": [ + "0x810A00Fa9287700871ba0f870Cd89D7Eac08D48C" + ], + "FEE_SETTER_ROLE": [], + "PAUSER_ROLE": [ + "0x810A00Fa9287700871ba0f870Cd89D7Eac08D48C" + ], + "UNPAUSER_ROLE": [ + "0x810A00Fa9287700871ba0f870Cd89D7Eac08D48C" + ], + "FUND_RESCUER_ROLE": [ + "0x810A00Fa9287700871ba0f870Cd89D7Eac08D48C" + ] } } \ No newline at end of file diff --git a/foundry/scripts/set-executors.js b/foundry/scripts/set-executors.js index a1b0d3c..9455af6 100644 --- a/foundry/scripts/set-executors.js +++ b/foundry/scripts/set-executors.js @@ -51,7 +51,7 @@ async function main() { // Set executors const executorAddresses = executorsToSet.map(executor => executor.executor); const tx = await router.setExecutors(executorAddresses, { - gasLimit: 200000 // should be around 50k per executor + gasLimit: 300000 // should be around 50k per executor }); await tx.wait(); // Wait for the transaction to be mined console.log(`Executors set at transaction: ${tx.hash}`); diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 0bce8dc..dcfe35d 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -111,8 +111,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { /** * @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits. * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount - * against a user-specified minimum. This function expects the input tokens to already be in the router at - * the time of calling. + * against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller. * * @dev * - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. @@ -144,7 +143,11 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { address receiver, bytes calldata swaps ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { - IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + if (address(tokenIn) != address(0)) { + IERC20(tokenIn).safeTransferFrom( + msg.sender, address(this), amountIn + ); + } return _splitSwapChecked( amountIn, tokenIn, @@ -897,6 +900,25 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { _handleCallback(fullData); } + /** + * @dev Called by PancakeV3 pool when swapping on it. + */ + function pancakeV3SwapCallback( + int256, /* amount0Delta */ + int256, /* amount1Delta */ + bytes calldata data + ) external { + if (data.length < 24) revert TychoRouter__InvalidDataLength(); + // We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data + // This way we preserve the bytes calldata (and don't need to convert it to bytes memory) + uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset + uint256 dataLength = + uint256(bytes32(msg.data[dataOffset:dataOffset + 32])); + + bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength]; + _handleCallback(fullData); + } + /** * @dev Called by UniswapV4 pool manager after achieving unlock state. */ @@ -908,4 +930,44 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { _handleCallback(data); return ""; } + + function locked(uint256) external { + address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D); + + // slither-disable-next-line controlled-delegatecall,low-level-calls + (bool success, bytes memory result) = executor.delegatecall(msg.data); + + if (!success) { + revert( + string( + result.length > 0 + ? result + : abi.encodePacked("Callback failed") + ) + ); + } + + // slither-disable-next-line assembly + assembly ("memory-safe") { + // Propagate the swappedAmount + return(add(result, 32), 16) + } + } + + function payCallback(uint256, address /*token*/ ) external { + address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D); + + // slither-disable-next-line controlled-delegatecall,low-level-calls + (bool success, bytes memory result) = executor.delegatecall(msg.data); + + if (!success) { + revert( + string( + result.length > 0 + ? result + : abi.encodePacked("Callback failed") + ) + ); + } + } } diff --git a/foundry/src/executors/EkuboExecutor.sol b/foundry/src/executors/EkuboExecutor.sol new file mode 100644 index 0000000..564b9d2 --- /dev/null +++ b/foundry/src/executors/EkuboExecutor.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IExecutor} from "@interfaces/IExecutor.sol"; +import {ICore} from "@ekubo/interfaces/ICore.sol"; +import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol"; +import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; +import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; +import {LibBytes} from "@solady/utils/LibBytes.sol"; +import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol"; +import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol"; + +contract EkuboExecutor is IExecutor, ILocker, IPayer { + error EkuboExecutor__InvalidDataLength(); + error EkuboExecutor__CoreOnly(); + error EkuboExecutor__UnknownCallback(); + + ICore immutable core; + + uint256 constant POOL_DATA_OFFSET = 92; + uint256 constant HOP_BYTE_LEN = 52; + + constructor(address _core) { + core = ICore(_core); + } + + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + if (data.length < 92) revert EkuboExecutor__InvalidDataLength(); + + // amountIn must be at most type(int128).MAX + calculatedAmount = + uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data))); + } + + function locked(uint256) external coreOnly { + int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52]))); + uint128 tokenInDebtAmount = uint128(nextAmountIn); + + address receiver = address(bytes20(msg.data[52:72])); + address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET])); + + address nextTokenIn = tokenIn; + + uint256 hopsLength = (msg.data.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN; + + uint256 offset = POOL_DATA_OFFSET; + + for (uint256 i = 0; i < hopsLength; i++) { + address nextTokenOut = + address(bytes20(LibBytes.loadCalldata(msg.data, offset))); + Config poolConfig = + Config.wrap(LibBytes.loadCalldata(msg.data, offset + 20)); + + (address token0, address token1, bool isToken1) = nextTokenIn + > nextTokenOut + ? (nextTokenOut, nextTokenIn, true) + : (nextTokenIn, nextTokenOut, false); + + // slither-disable-next-line calls-loop + (int128 delta0, int128 delta1) = core.swap_611415377( + EkuboPoolKey(token0, token1, poolConfig), + nextAmountIn, + isToken1, + isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO, + 0 + ); + + nextTokenIn = nextTokenOut; + nextAmountIn = -(isToken1 ? delta0 : delta1); + + offset += HOP_BYTE_LEN; + } + + _pay(tokenIn, tokenInDebtAmount); + + core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn)); + + // slither-disable-next-line assembly + assembly ("memory-safe") { + mstore(0, nextAmountIn) + return(0x10, 16) + } + } + + function payCallback(uint256, address token) external coreOnly { + uint128 amount = uint128(bytes16(msg.data[68:84])); + + SafeTransferLib.safeTransfer(token, address(core), amount); + } + + function _lock(bytes memory data) + internal + returns (uint128 swappedAmount) + { + address target = address(core); + + // slither-disable-next-line assembly + assembly ("memory-safe") { + let args := mload(0x40) + + // Selector of lock() + mstore(args, shl(224, 0xf83d08ba)) + + // We only copy the data, not the length, because the length is read from the calldata size + let len := mload(data) + mcopy(add(args, 4), add(data, 32), len) + + // If the call failed, pass through the revert + if iszero(call(gas(), target, 0, args, add(len, 36), 0, 0)) { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + + returndatacopy(0, 0, 16) + swappedAmount := shr(128, mload(0)) + } + } + + function _pay(address token, uint128 amount) internal { + address target = address(core); + + if (token == NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeTransferETH(target, amount); + } else { + // slither-disable-next-line assembly + assembly ("memory-safe") { + let free := mload(0x40) + // selector of pay(address) + mstore(free, shl(224, 0x0c11dedd)) + mstore(add(free, 4), token) + mstore(add(free, 36), shl(128, amount)) + + // if it failed, pass through revert + if iszero(call(gas(), target, 0, free, 52, 0, 0)) { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + } + + // To receive withdrawals from Core + receive() external payable {} + + modifier coreOnly() { + if (msg.sender != address(core)) revert EkuboExecutor__CoreOnly(); + _; + } +} diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 6707ce8..50fb056 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -8,21 +8,24 @@ import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol"; error UniswapV2Executor__InvalidDataLength(); error UniswapV2Executor__InvalidTarget(); error UniswapV2Executor__InvalidFactory(); +error UniswapV2Executor__InvalidInitCode(); contract UniswapV2Executor is IExecutor { using SafeERC20 for IERC20; - bytes32 internal constant POOL_INIT_CODE_HASH = - 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; - address public immutable factory; + bytes32 public immutable initCode; address private immutable self; - constructor(address _factory) { + constructor(address _factory, bytes32 _initCode) { if (_factory == address(0)) { revert UniswapV2Executor__InvalidFactory(); } + if (_initCode == bytes32(0)) { + revert UniswapV2Executor__InvalidInitCode(); + } factory = _factory; + initCode = _initCode; self = address(this); } @@ -102,9 +105,7 @@ contract UniswapV2Executor is IExecutor { uint160( uint256( keccak256( - abi.encodePacked( - hex"ff", factory, salt, POOL_INIT_CODE_HASH - ) + abi.encodePacked(hex"ff", factory, salt, initCode) ) ) ) diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index c03b5d3..1d46c08 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -4,30 +4,33 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "@interfaces/ICallback.sol"; error UniswapV3Executor__InvalidDataLength(); error UniswapV3Executor__InvalidFactory(); error UniswapV3Executor__InvalidTarget(); +error UniswapV3Executor__InvalidInitCode(); contract UniswapV3Executor is IExecutor, ICallback { using SafeERC20 for IERC20; - bytes32 internal constant POOL_INIT_CODE_HASH = - 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; uint160 private constant MIN_SQRT_RATIO = 4295128739; uint160 private constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; address public immutable factory; + bytes32 public immutable initCode; address private immutable self; - constructor(address _factory) { + constructor(address _factory, bytes32 _initCode) { if (_factory == address(0)) { revert UniswapV3Executor__InvalidFactory(); } + if (_initCode == bytes32(0)) { + revert UniswapV3Executor__InvalidInitCode(); + } factory = _factory; + initCode = _initCode; self = address(this); } @@ -102,8 +105,7 @@ contract UniswapV3Executor is IExecutor, ICallback { address tokenOut = address(bytes20(data[20:40])); uint24 poolFee = uint24(bytes3(data[40:43])); - // slither-disable-next-line unused-return - CallbackValidationV2.verifyCallback(factory, tokenIn, tokenOut, poolFee); + _verifyPairAddress(tokenIn, tokenOut, poolFee, msg.sender); } function uniswapV3SwapCallback( @@ -167,7 +169,7 @@ contract UniswapV3Executor is IExecutor, ICallback { hex"ff", factory, keccak256(abi.encode(token0, token1, fee)), - POOL_INIT_CODE_HASH + initCode ) ) ) diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 9149dcf..1a1fff7 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -47,19 +47,53 @@ contract Constants is Test, BaseConstants { address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416; address USDC_WETH_USV2 = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; + // Sushiswap v2 + address SUSHISWAP_WBTC_WETH_POOL = + 0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58; + + // Pancakeswap v2 + address PANCAKESWAP_WBTC_WETH_POOL = + 0x4AB6702B3Ed3877e9b1f203f90cbEF13d663B0e8; + // Uniswap v3 - address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984; - address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; address USDC_WETH_USV3 = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% fee address USDC_WETH_USV3_2 = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8; // 0.3% fee + // Pancakeswap v3 + address PANCAKESWAPV3_WETH_USDT_POOL = + 0x6CA298D2983aB03Aa1dA7679389D955A4eFEE15C; + + // Factories + address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address SUSHISWAPV2_FACTORY_ETHEREUM = + 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac; + address PANCAKESWAPV2_FACTORY_ETHEREUM = + 0x1097053Fd2ea711dad45caCcc45EfF7548fCB362; + + // Pancakeswap uses their deployer instead of their factory for target verification + address PANCAKESWAPV3_DEPLOYER_ETHEREUM = + 0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9; + // Uniswap universal router address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; // Permit2 address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + // Pool Code Init Hashes + bytes32 USV2_POOL_CODE_INIT_HASH = + 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; + bytes32 USV3_POOL_CODE_INIT_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + bytes32 SUSHIV2_POOL_CODE_INIT_HASH = + 0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303; + bytes32 PANCAKEV2_POOL_CODE_INIT_HASH = + 0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d; + bytes32 PANCAKEV3_POOL_CODE_INIT_HASH = + 0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2; + /** * @dev Deploys a dummy contract with non-empty bytecode */ diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index edbbe54..2f79f01 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; +import "../src/executors/BalancerV2Executor.sol"; +import "../src/executors/EkuboExecutor.sol"; import "../src/executors/UniswapV2Executor.sol"; import "../src/executors/UniswapV3Executor.sol"; import "../src/executors/UniswapV4Executor.sol"; @@ -8,8 +10,8 @@ import "./Constants.sol"; import "./mock/MockERC20.sol"; import "@src/TychoRouter.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; contract TychoRouterExposed is TychoRouter { constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} @@ -43,7 +45,10 @@ contract TychoRouterTestSetup is Constants { address tychoRouterAddr; UniswapV2Executor public usv2Executor; UniswapV3Executor public usv3Executor; + UniswapV3Executor public pancakev3Executor; UniswapV4Executor public usv4Executor; + BalancerV2Executor public balancerv2Executor; + EkuboExecutor public ekuboExecutor; MockERC20[] tokens; function setUp() public { @@ -51,10 +56,23 @@ contract TychoRouterTestSetup is Constants { vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.startPrank(ADMIN); - address factoryV3 = USV3_FACTORY_ETHEREUM; - address factoryV2 = USV2_FACTORY_ETHEREUM; - address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; - IPoolManager poolManager = IPoolManager(poolManagerAddress); + tychoRouter = deployRouter(); + deployDummyContract(); + vm.stopPrank(); + + address[] memory executors = deployExecutors(); + vm.startPrank(EXECUTOR_SETTER); + tychoRouter.setExecutors(executors); + vm.stopPrank(); + + vm.startPrank(BOB); + tokens.push(new MockERC20("Token A", "A")); + tokens.push(new MockERC20("Token B", "B")); + tokens.push(new MockERC20("Token C", "C")); + vm.stopPrank(); + } + + function deployRouter() public returns (TychoRouterExposed) { tychoRouter = new TychoRouterExposed(PERMIT2_ADDRESS, WETH_ADDR); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); @@ -64,25 +82,36 @@ contract TychoRouterTestSetup is Constants { tychoRouter.grantRole( keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER ); - deployDummyContract(); - vm.stopPrank(); + return tychoRouter; + } - usv2Executor = new UniswapV2Executor(factoryV2); - usv3Executor = new UniswapV3Executor(factoryV3); + function deployExecutors() public returns (address[] memory) { + address factoryV2 = USV2_FACTORY_ETHEREUM; + address factoryV3 = USV3_FACTORY_ETHEREUM; + address factoryPancakeV3 = PANCAKESWAPV3_DEPLOYER_ETHEREUM; + bytes32 initCodeV2 = USV2_POOL_CODE_INIT_HASH; + bytes32 initCodeV3 = USV3_POOL_CODE_INIT_HASH; + bytes32 initCodePancakeV3 = PANCAKEV3_POOL_CODE_INIT_HASH; + address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; + address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; + + IPoolManager poolManager = IPoolManager(poolManagerAddress); + usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2); + usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3); usv4Executor = new UniswapV4Executor(poolManager); - vm.startPrank(EXECUTOR_SETTER); - address[] memory executors = new address[](3); + pancakev3Executor = + new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3); + balancerv2Executor = new BalancerV2Executor(); + ekuboExecutor = new EkuboExecutor(ekuboCore); + + address[] memory executors = new address[](6); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); - executors[2] = address(usv4Executor); - tychoRouter.setExecutors(executors); - vm.stopPrank(); - - vm.startPrank(BOB); - tokens.push(new MockERC20("Token A", "A")); - tokens.push(new MockERC20("Token B", "B")); - tokens.push(new MockERC20("Token C", "C")); - vm.stopPrank(); + executors[2] = address(pancakev3Executor); + executors[3] = address(usv4Executor); + executors[4] = address(balancerv2Executor); + executors[5] = address(ekuboExecutor); + return executors; } /** diff --git a/foundry/test/executors/EkuboExecutor.t.sol b/foundry/test/executors/EkuboExecutor.t.sol new file mode 100644 index 0000000..0971b40 --- /dev/null +++ b/foundry/test/executors/EkuboExecutor.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import {EkuboExecutor} from "@src/executors/EkuboExecutor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Constants} from "../Constants.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol"; +import {ICore} from "@ekubo/interfaces/ICore.sol"; + +contract EkuboExecutorTest is Test, Constants { + address constant EXECUTOR_ADDRESS = + 0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests + EkuboExecutor executor; + + IERC20 USDC = IERC20(USDC_ADDR); + IERC20 USDT = IERC20(USDT_ADDR); + + address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; + + bytes32 constant ORACLE_CONFIG = + 0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754); + + deployCodeTo( + "executors/EkuboExecutor.sol", + abi.encode(CORE_ADDRESS), + EXECUTOR_ADDRESS + ); + executor = EkuboExecutor(payable(EXECUTOR_ADDRESS)); + } + + function testSingleSwapEth() public { + uint256 amountIn = 1 ether; + + deal(address(executor), amountIn); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS); + uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); + + bytes memory data = abi.encodePacked( + address(executor), // receiver + NATIVE_TOKEN_ADDRESS, // tokenIn + USDC_ADDR, // tokenOut + ORACLE_CONFIG // poolConfig + ); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + + assertEq( + USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut + ); + assertEq( + USDC.balanceOf(address(executor)), + usdcBalanceBeforeExecutor + amountOut + ); + } + + function testSingleSwapERC20() public { + uint256 amountIn = 1_000_000_000; + + deal(USDC_ADDR, address(executor), amountIn); + + uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS); + uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor)); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + bytes memory data = abi.encodePacked( + address(executor), // receiver + USDC_ADDR, // tokenIn + NATIVE_TOKEN_ADDRESS, // tokenOut + ORACLE_CONFIG // config + ); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn); + assertEq( + USDC.balanceOf(address(executor)), + usdcBalanceBeforeExecutor - amountIn + ); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut); + assertEq( + address(executor).balance, ethBalanceBeforeExecutor + amountOut + ); + } + + // Expects input that encodes the same test case as swap_encoder::tests::ekubo::test_encode_swap_multi + function multiHopSwap(bytes memory data) internal { + uint256 amountIn = 1 ether; + + deal(address(executor), amountIn); + + uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance; + uint256 ethBalanceBeforeExecutor = address(executor).balance; + + uint256 usdtBalanceBeforeCore = USDT.balanceOf(CORE_ADDRESS); + uint256 usdtBalanceBeforeExecutor = USDT.balanceOf(address(executor)); + + uint256 gasBefore = gasleft(); + uint256 amountOut = executor.swap(amountIn, data); + console.log(gasBefore - gasleft()); + + console.log(amountOut); + + assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn); + assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn); + + assertEq( + USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut + ); + assertEq( + USDT.balanceOf(address(executor)), + usdtBalanceBeforeExecutor + amountOut + ); + } + + // Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi + function testMultiHopSwap() public { + bytes memory data = abi.encodePacked( + address(executor), // receiver + NATIVE_TOKEN_ADDRESS, // tokenIn + USDC_ADDR, // tokenOut of 1st swap + ORACLE_CONFIG, // config of 1st swap + USDT_ADDR, // tokenOut of 2nd swap + bytes32( + 0x00000000000000000000000000000000000000000001a36e2eb1c43200000032 + ) // config of 2nd swap (0.0025% fee & 0.005% base pool) + ); + multiHopSwap(data); + } + + // Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi + function testMultiHopSwapIntegration() public { + multiHopSwap( + hex"ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032" + ); + } +} diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index f9b15a4..a26a9b7 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -6,7 +6,9 @@ import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; contract UniswapV2ExecutorExposed is UniswapV2Executor { - constructor(address _factory) UniswapV2Executor(_factory) {} + constructor(address _factory, bytes32 _initCode) + UniswapV2Executor(_factory, _initCode) + {} function decodeParams(bytes calldata data) external @@ -48,13 +50,23 @@ contract UniswapV2ExecutorTest is Test, Constants { using SafeERC20 for IERC20; UniswapV2ExecutorExposed uniswapV2Exposed; + UniswapV2ExecutorExposed sushiswapV2Exposed; + UniswapV2ExecutorExposed pancakeswapV2Exposed; IERC20 WETH = IERC20(WETH_ADDR); IERC20 DAI = IERC20(DAI_ADDR); function setUp() public { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV2Exposed = new UniswapV2ExecutorExposed(USV2_FACTORY_ETHEREUM); + uniswapV2Exposed = new UniswapV2ExecutorExposed( + USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH + ); + sushiswapV2Exposed = new UniswapV2ExecutorExposed( + SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH + ); + pancakeswapV2Exposed = new UniswapV2ExecutorExposed( + PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH + ); } function testDecodeParams() public view { @@ -82,6 +94,14 @@ contract UniswapV2ExecutorTest is Test, Constants { uniswapV2Exposed.verifyPairAddress(WETH_DAI_POOL); } + function testVerifyPairAddressSushi() public view { + sushiswapV2Exposed.verifyPairAddress(SUSHISWAP_WBTC_WETH_POOL); + } + + function testVerifyPairAddressPancake() public view { + pancakeswapV2Exposed.verifyPairAddress(PANCAKESWAP_WBTC_WETH_POOL); + } + function testInvalidTarget() public { address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR)); vm.expectRevert(UniswapV2Executor__InvalidTarget.selector); diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index 8928627..01f5d1c 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -6,7 +6,9 @@ import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; contract UniswapV3ExecutorExposed is UniswapV3Executor { - constructor(address _factory) UniswapV3Executor(_factory) {} + constructor(address _factory, bytes32 _initCode) + UniswapV3Executor(_factory, _initCode) + {} function decodeData(bytes calldata data) external @@ -37,6 +39,7 @@ contract UniswapV3ExecutorTest is Test, Constants { using SafeERC20 for IERC20; UniswapV3ExecutorExposed uniswapV3Exposed; + UniswapV3ExecutorExposed pancakeV3Exposed; IERC20 WETH = IERC20(WETH_ADDR); IERC20 DAI = IERC20(DAI_ADDR); @@ -44,7 +47,12 @@ contract UniswapV3ExecutorTest is Test, Constants { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV3Exposed = new UniswapV3ExecutorExposed(USV3_FACTORY_ETHEREUM); + uniswapV3Exposed = new UniswapV3ExecutorExposed( + USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH + ); + pancakeV3Exposed = new UniswapV3ExecutorExposed( + PANCAKESWAPV3_DEPLOYER_ETHEREUM, PANCAKEV3_POOL_CODE_INIT_HASH + ); } function testDecodeParams() public view { @@ -84,6 +92,12 @@ contract UniswapV3ExecutorTest is Test, Constants { ); } + function testVerifyPairAddressPancake() public view { + pancakeV3Exposed.verifyPairAddress( + WETH_ADDR, USDT_ADDR, 500, PANCAKESWAPV3_WETH_USDT_POOL + ); + } + function testUSV3Callback() public { uint24 poolFee = 3000; uint256 amountOwed = 1000000000000000000; diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 06cbe7b..c0216fa 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -1,7 +1,7 @@ use std::io::{self, Read}; use clap::{Parser, Subcommand}; -use tycho_core::models::Chain; +use tycho_common::{hex_bytes::Bytes, models::Chain}; use tycho_execution::encoding::{ evm::encoder_builder::EVMEncoderBuilder, models::Solution, tycho_encoder::TychoEncoder, }; @@ -35,7 +35,6 @@ use tycho_execution::encoding::{ /// "token_out": "0x...", /// "split": 0.0 /// }], -/// "router_address": "0x..." /// } /// ``` #[command(author, version, about, long_about = None)] @@ -44,6 +43,8 @@ pub struct Cli { pub command: Commands, #[arg(short, long)] executors_file_path: Option, + #[arg(short, long)] + router_address: Option, } #[derive(Subcommand)] @@ -79,6 +80,9 @@ fn main() -> Result<(), Box> { if let Some(config_path) = cli.executors_file_path { builder = builder.executors_file_path(config_path); } + if let Some(router_address) = cli.router_address { + builder = builder.router_address(router_address); + } builder = match cli.command { Commands::TychoRouter => builder.initialize_tycho_router()?, diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index f1a5153..549197e 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -15,7 +15,7 @@ use tokio::{ runtime::{Handle, Runtime}, task::block_in_place, }; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, @@ -175,7 +175,7 @@ mod tests { use alloy_primitives::Uint; use num_bigint::BigUint; - use tycho_core::models::Chain as TychoCoreChain; + use tycho_common::models::Chain as TychoCoreChain; use super::*; diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index 8f93215..b3d33fc 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -1,6 +1,7 @@ use std::{collections::HashSet, sync::LazyLock}; pub const DEFAULT_EXECUTORS_JSON: &str = include_str!("../../../config/executor_addresses.json"); +pub const DEFAULT_ROUTERS_JSON: &str = include_str!("../../../config/router_addresses.json"); /// These protocols support the optimization of grouping swaps. /// @@ -11,5 +12,6 @@ pub static GROUPABLE_PROTOCOLS: LazyLock> = LazyLock::new( let mut set = HashSet::new(); set.insert("uniswap_v4"); set.insert("balancer_v3"); + set.insert("ekubo_v2"); set }); diff --git a/src/encoding/evm/encoder_builder.rs b/src/encoding/evm/encoder_builder.rs index 11a5f2e..e20c148 100644 --- a/src/encoding/evm/encoder_builder.rs +++ b/src/encoding/evm/encoder_builder.rs @@ -1,4 +1,4 @@ -use tycho_core::models::Chain; +use tycho_common::{models::Chain, Bytes}; use crate::encoding::{ errors::EncodingError, @@ -17,6 +17,7 @@ pub struct EVMEncoderBuilder { strategy: Option>, chain: Option, executors_file_path: Option, + router_address: Option, } impl Default for EVMEncoderBuilder { @@ -27,7 +28,12 @@ impl Default for EVMEncoderBuilder { impl EVMEncoderBuilder { pub fn new() -> Self { - EVMEncoderBuilder { chain: None, strategy: None, executors_file_path: None } + EVMEncoderBuilder { + chain: None, + strategy: None, + executors_file_path: None, + router_address: None, + } } pub fn chain(mut self, chain: Chain) -> Self { self.chain = Some(chain); @@ -41,6 +47,13 @@ impl EVMEncoderBuilder { self } + /// Sets the `router_address` manually. + /// If it's not set, the default router address will be used (config/router_addresses.json) + pub fn router_address(mut self, router_address: Bytes) -> Self { + self.router_address = Some(router_address); + self + } + /// Sets the `strategy_encoder` manually. /// /// **Note**: This method should not be used in combination with `tycho_router` or @@ -56,12 +69,17 @@ impl EVMEncoderBuilder { if let Some(chain) = self.chain { let swap_encoder_registry = SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = - Box::new(SplitSwapStrategyEncoder::new(chain, swap_encoder_registry, None)?); + let strategy = Box::new(SplitSwapStrategyEncoder::new( + chain, + swap_encoder_registry, + None, + self.router_address.clone(), + )?); Ok(EVMEncoderBuilder { chain: Some(chain), strategy: Some(strategy), executors_file_path: self.executors_file_path, + router_address: self.router_address, }) } else { Err(EncodingError::FatalError( @@ -83,11 +101,13 @@ impl EVMEncoderBuilder { chain, swap_encoder_registry, Some(swapper_pk), + self.router_address.clone(), )?); Ok(EVMEncoderBuilder { chain: Some(chain), strategy: Some(strategy), executors_file_path: self.executors_file_path, + router_address: self.router_address, }) } else { Err(EncodingError::FatalError( @@ -107,6 +127,7 @@ impl EVMEncoderBuilder { chain: Some(chain), strategy: Some(strategy), executors_file_path: self.executors_file_path, + router_address: self.router_address, }) } else { Err(EncodingError::FatalError( diff --git a/src/encoding/evm/strategy_encoder/group_swaps.rs b/src/encoding/evm/strategy_encoder/group_swaps.rs index 48cb86b..0e4206c 100644 --- a/src/encoding/evm/strategy_encoder/group_swaps.rs +++ b/src/encoding/evm/strategy_encoder/group_swaps.rs @@ -1,4 +1,4 @@ -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap}; @@ -74,7 +74,7 @@ mod tests { use std::str::FromStr; use alloy_primitives::hex; - use tycho_core::{models::protocol::ProtocolComponent, Bytes}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; use crate::encoding::models::Swap; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index a3e48d2..891e3c2 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1,13 +1,17 @@ -use std::{collections::HashSet, str::FromStr}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; use alloy_primitives::{aliases::U24, U256, U8}; use alloy_sol_types::SolValue; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, + constants::DEFAULT_ROUTERS_JSON, strategy_encoder::{group_swaps::group_swaps, strategy_validators::SplitSwapValidator}, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ @@ -68,6 +72,7 @@ pub trait EVMStrategyEncoder: StrategyEncoder { /// * `wrapped_address`: Address of the chain's wrapped token /// * `split_swap_validator`: SplitSwapValidator, responsible for checking validity of split swap /// solutions +/// * `router_address`: Address of the router to be used to execute swaps #[derive(Clone)] pub struct SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, @@ -76,13 +81,15 @@ pub struct SplitSwapStrategyEncoder { native_address: Bytes, wrapped_address: Bytes, split_swap_validator: SplitSwapValidator, + router_address: Bytes, } impl SplitSwapStrategyEncoder { pub fn new( - blockchain: tycho_core::models::Chain, + blockchain: tycho_common::models::Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, + router_address: Option, ) -> Result { let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { @@ -94,6 +101,21 @@ impl SplitSwapStrategyEncoder { .to_string(), ) }; + + let tycho_router_address; + if let Some(address) = router_address { + tycho_router_address = address; + } else { + let default_routers: HashMap = + serde_json::from_str(DEFAULT_ROUTERS_JSON)?; + tycho_router_address = default_routers + .get(&chain.name) + .ok_or(EncodingError::FatalError( + "No default router address found for chain".to_string(), + ))? + .to_owned(); + } + Ok(Self { permit2, selector, @@ -101,6 +123,7 @@ impl SplitSwapStrategyEncoder { native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, split_swap_validator: SplitSwapValidator, + router_address: tycho_router_address, }) } } @@ -183,9 +206,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let encoding_context = EncodingContext { - receiver: solution.router_address.clone(), + receiver: self.router_address.clone(), exact_out: solution.exact_out, - router_address: solution.router_address.clone(), + router_address: Some(self.router_address.clone()), group_token_in: grouped_swap.input_token.clone(), group_token_out: grouped_swap.output_token.clone(), }; @@ -214,7 +237,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { }; let method_calldata = if let Some(permit2) = self.permit2.clone() { let (permit, signature) = permit2.get_permit( - &solution.router_address, + &self.router_address, &solution.sender, &solution.given_token, &solution.given_amount, @@ -249,7 +272,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { }; let contract_interaction = encode_input(&self.selector, method_calldata); - Ok((contract_interaction, solution.router_address)) + Ok((contract_interaction, self.router_address.clone())) } fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { @@ -294,7 +317,6 @@ impl StrategyEncoder for ExecutorStrategyEncoder { .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; let receiver = solution.receiver; - let router_address = solution.router_address; let swap_encoder = self .get_swap_encoder(&grouped_swap.protocol_system) @@ -310,7 +332,7 @@ impl StrategyEncoder for ExecutorStrategyEncoder { let encoding_context = EncodingContext { receiver: receiver.clone(), exact_out: solution.exact_out, - router_address: router_address.clone(), + router_address: None, group_token_in: grouped_swap.input_token.clone(), group_token_out: grouped_swap.output_token.clone(), }; @@ -339,10 +361,10 @@ mod tests { use std::{collections::HashMap, str::FromStr}; use alloy::hex::encode; - use alloy_primitives::hex; + use alloy_primitives::{hex, Address}; use num_bigint::{BigInt, BigUint}; use rstest::rstest; - use tycho_core::{ + use tycho_common::{ models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, Bytes, }; @@ -364,7 +386,8 @@ mod tests { fn get_swap_encoder_registry() -> SwapEncoderRegistry { let eth_chain = eth_chain(); - SwapEncoderRegistry::new(None, eth_chain).unwrap() + SwapEncoderRegistry::new(Some("config/test_executor_addresses.json".to_string()), eth_chain) + .unwrap() } #[test] @@ -397,7 +420,6 @@ mod tests { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), swaps: vec![swap], - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), slippage: None, native_action: None, }; @@ -408,7 +430,7 @@ mod tests { let hex_protocol_data = encode(&protocol_data); assert_eq!( executor_address, - Bytes::from_str("0xf6c5be66FFf9DC69962d73da0A617a827c382329").unwrap() + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() ); assert_eq!( hex_protocol_data, @@ -453,7 +475,6 @@ mod tests { sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), swaps: vec![swap.clone(), swap], - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), slippage: None, native_action: None, }; @@ -522,7 +543,6 @@ mod tests { slippage: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap_usdc_eth, swap_eth_pepe], ..Default::default() }; @@ -533,7 +553,7 @@ mod tests { let hex_protocol_data = encode(&protocol_data); assert_eq!( executor_address, - Bytes::from_str("0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70").unwrap() + Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() ); assert_eq!( hex_protocol_data, @@ -545,7 +565,7 @@ mod tests { // zero for one "00", // executor address - "042c0ebbeab9d9987c2f64ee05f2b3aeb86eaf70", + "f62849f9a0b5bf2913b396098f7c7019b51a820a", // first pool intermediary token (ETH) "0000000000000000000000000000000000000000", // fee @@ -607,9 +627,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -620,7 +644,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; @@ -671,7 +694,7 @@ mod tests { "01", // token out index "000000", // split // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver @@ -708,9 +731,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: eth(), @@ -720,7 +747,6 @@ mod tests { checked_amount: Some(BigUint::from_str("2659881924818443699787").unwrap()), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], native_action: Some(NativeAction::Wrap), ..Default::default() @@ -757,9 +783,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: dai, @@ -769,7 +799,6 @@ mod tests { checked_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], native_action: Some(NativeAction::Unwrap), ..Default::default() @@ -847,9 +876,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -859,7 +892,6 @@ mod tests { checked_amount: Some(BigUint::from_str("26173932").unwrap()), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc], ..Default::default() }; @@ -928,9 +960,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: usdc, @@ -941,7 +977,6 @@ mod tests { slippage: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap_usdc_eth, swap_eth_pepe], ..Default::default() }; @@ -992,12 +1027,12 @@ mod tests { "01", // token out index "000000", // split // Swap data header - "042c0ebbeab9d9987c2f64ee05f2b3aeb86eaf70", // executor address + "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address // Protocol data "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // group token in "6982508145454ce325ddbe47a25d4ec3d2311933", // group token in "00", // zero2one - "042c0ebbeab9d9987c2f64ee05f2b3aeb86eaf70", // executor address + "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address // First pool params "0000000000000000000000000000000000000000", // intermediary token (ETH) "000bb8", // fee @@ -1013,6 +1048,67 @@ mod tests { assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); + println!("{}", hex_calldata); + } + + #[test] + fn test_split_encoding_strategy_ekubo() { + // ETH ──(EKUBO)──> USDC + + let token_in = Bytes::from(Address::ZERO.as_slice()); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let static_attributes = HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ("extension".to_string(), Bytes::from("0x51d02a5948496a67827242eabc5725531342527c")), /* Oracle */ + ]); + + let component = ProtocolComponent { + // All Ekubo swaps go through the core contract - not necessary to specify pool id + // for test + protocol_system: "ekubo_v2".to_string(), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Some(Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap()), + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1").unwrap()), + slippage: None, + // Alice + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); } #[test] @@ -1039,8 +1135,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, None).unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -1051,7 +1152,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; @@ -1078,7 +1178,7 @@ mod tests { "01", // token out index "000000", // split // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver @@ -1128,9 +1228,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, @@ -1142,7 +1246,6 @@ mod tests { slippage: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap_eth_pepe], ..Default::default() }; @@ -1192,9 +1295,13 @@ mod tests { }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, @@ -1206,7 +1313,6 @@ mod tests { slippage: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap_usdc_eth], ..Default::default() }; @@ -1276,9 +1382,13 @@ mod tests { }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + ) + .unwrap(); let solution = Solution { exact_out: false, @@ -1290,7 +1400,6 @@ mod tests { * test */ slippage: None, swaps: vec![swap_usdc_weth, swap_weth_usdc], - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), ..Default::default() @@ -1319,7 +1428,7 @@ mod tests { "00", // token in index "01", // token out index "000000", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out "0001f4", // pool fee @@ -1329,7 +1438,7 @@ mod tests { "006d", // ple encoded swaps "01", // token in index "00000000", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out "000bb8", // pool fee @@ -1341,6 +1450,7 @@ mod tests { assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); + println!("{}", hex_calldata); } #[test] @@ -1428,6 +1538,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key.clone()), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), ) .unwrap(); @@ -1439,7 +1550,6 @@ mod tests { expected_amount: None, checked_amount: Some(BigUint::from_str("99574171").unwrap()), /* Expected output from * test */ - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), slippage: None, @@ -1470,7 +1580,7 @@ mod tests { "00", // token in index "01", // token out index "999999", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out "0001f4", // pool fee @@ -1481,7 +1591,7 @@ mod tests { "00", // token in index "01", // token out index "000000", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out "000bb8", // pool fee @@ -1492,7 +1602,7 @@ mod tests { "01", // token in index "00", // token out index "000000", // split - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address, + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address, "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id, "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address @@ -1502,6 +1612,7 @@ mod tests { .join(""); assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); + println!("{}", hex_calldata); } #[test] @@ -1585,6 +1696,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key.clone()), + Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), ) .unwrap(); @@ -1596,7 +1708,6 @@ mod tests { expected_amount: None, checked_amount: Some(BigUint::from_str("99525908").unwrap()), /* Expected output from * test */ - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), slippage: None, @@ -1628,7 +1739,7 @@ mod tests { "00", // token in index "01", // token out index "000000", // split - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address @@ -1637,7 +1748,7 @@ mod tests { "01", // token in index "00", // token out index "999999", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out "0001f4", // pool fee @@ -1648,7 +1759,7 @@ mod tests { "01", // token in index "00", // token out index "000000", // split - "dd8559c917393fc8dd2b4dd289c52ff445fde1b0", // executor address + "2e234dae75c793f67a35089c9d99245e1c58470b", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token out "000bb8", // pool fee @@ -1661,5 +1772,6 @@ mod tests { assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); + println!("{}", hex_calldata); } } diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index d4bf22f..e7ae722 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet, VecDeque}; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, @@ -203,7 +203,7 @@ mod tests { use num_bigint::BigUint; use rstest::rstest; - use tycho_core::{models::protocol::ProtocolComponent, Bytes}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; use crate::encoding::models::Swap; diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index 075dad0..65bc317 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -1,7 +1,8 @@ use crate::encoding::{ errors::EncodingError, evm::swap_encoder::swap_encoders::{ - BalancerV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder, + BalancerV2SwapEncoder, EkuboSwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, + UniswapV4SwapEncoder, }, swap_encoder::SwapEncoder, }; @@ -23,9 +24,13 @@ impl SwapEncoderBuilder { pub fn build(self) -> Result, EncodingError> { match self.protocol_system.as_str() { "uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), + "sushiswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), + "pancakeswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), "vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address))), "uniswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), + "pancakeswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), "uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address))), + "ekubo_v2" => Ok(Box::new(EkuboSwapEncoder::new(self.executor_address))), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}", self.protocol_system diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 72ab48a..8a5b9e4 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -19,7 +19,7 @@ impl SwapEncoderRegistry { /// executors' addresses in the file at the given path. pub fn new( executors_file_path: Option, - blockchain: tycho_core::models::Chain, + blockchain: tycho_common::models::Chain, ) -> Result { let chain = Chain::from(blockchain); let config_str = if let Some(ref path) = executors_file_path { diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 473b250..ad6ffc9 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, @@ -229,13 +229,19 @@ impl SwapEncoder for BalancerV2SwapEncoder { ) -> Result, EncodingError> { let token_approvals_manager = ProtocolApprovalsManager::new()?; let token = bytes_to_address(&swap.token_in)?; - let router_address = bytes_to_address(&encoding_context.router_address)?; - let approval_needed = token_approvals_manager.approval_needed( - token, - router_address, - Address::from_str(&self.vault_address) - .map_err(|_| EncodingError::FatalError("Invalid vault address".to_string()))?, - )?; + let approval_needed: bool; + + if let Some(router_address) = encoding_context.router_address { + let tycho_router_address = bytes_to_address(&router_address)?; + approval_needed = token_approvals_manager.approval_needed( + token, + tycho_router_address, + Address::from_str(&self.vault_address) + .map_err(|_| EncodingError::FatalError("Invalid vault address".to_string()))?, + )?; + } else { + approval_needed = true; + } let component_id = AlloyBytes::from_str(&swap.component.id) .map_err(|_| EncodingError::FatalError("Invalid component ID".to_string()))?; @@ -258,13 +264,77 @@ impl SwapEncoder for BalancerV2SwapEncoder { } } +/// Encodes a swap on an Ekubo pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EkuboSwapEncoder { + executor_address: String, +} + +impl SwapEncoder for EkuboSwapEncoder { + fn new(executor_address: String) -> Self { + Self { executor_address } + } + + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + if encoding_context.exact_out { + return Err(EncodingError::InvalidInput("exact out swaps not implemented".to_string())); + } + + let fee = u64::from_be_bytes( + get_static_attribute(&swap, "fee")? + .try_into() + .map_err(|_| EncodingError::FatalError("fee should be an u64".to_string()))?, + ); + + let tick_spacing = u32::from_be_bytes( + get_static_attribute(&swap, "tick_spacing")? + .try_into() + .map_err(|_| { + EncodingError::FatalError("tick_spacing should be an u32".to_string()) + })?, + ); + + let extension: Address = get_static_attribute(&swap, "extension")? + .as_slice() + .try_into() + .map_err(|_| EncodingError::FatalError("extension should be an address".to_string()))?; + + let mut encoded = vec![]; + + if encoding_context.group_token_in == swap.token_in { + encoded.extend(bytes_to_address(&encoding_context.receiver)?); + encoded.extend(bytes_to_address(&swap.token_in)?); + } + + encoded.extend(bytes_to_address(&swap.token_out)?); + encoded.extend((extension, fee, tick_spacing).abi_encode_packed()); + + Ok(encoded) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; use alloy::hex::encode; use num_bigint::BigInt; - use tycho_core::{models::protocol::ProtocolComponent, Bytes}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; @@ -286,7 +356,7 @@ mod tests { let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, - router_address: Bytes::zero(20), + router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; @@ -333,7 +403,7 @@ mod tests { let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, - router_address: Bytes::zero(20), + router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; @@ -381,7 +451,7 @@ mod tests { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), exact_out: false, - router_address: Bytes::zero(20), + router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; @@ -439,7 +509,7 @@ mod tests { receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), exact_out: false, // Same as the executor address - router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + router_address: Some(Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")), group_token_in: token_in.clone(), group_token_out: token_out.clone(), @@ -503,7 +573,7 @@ mod tests { let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, - router_address: Bytes::zero(20), + router_address: Some(Bytes::zero(20)), group_token_in: group_token_in.clone(), // Token out is the same as the group token out group_token_out: token_out.clone(), @@ -542,7 +612,7 @@ mod tests { let context = EncodingContext { receiver: receiver_address.clone(), exact_out: false, - router_address: router_address.clone(), + router_address: Some(router_address.clone()), group_token_in: usde_address.clone(), group_token_out: wbtc_address.clone(), }; @@ -636,4 +706,142 @@ mod tests { )) ); } + + mod ekubo { + use super::*; + + const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address + + #[test] + fn test_encode_swap_simple() { + let token_in = Bytes::from(Address::ZERO.as_slice()); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let static_attributes = HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ( + "extension".to_string(), + Bytes::from("0x51d02a5948496a67827242eabc5725531342527c"), + ), // Oracle + ]); + + let component = ProtocolComponent { static_attributes, ..Default::default() }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let encoding_context = EncodingContext { + receiver: RECEIVER.into(), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + exact_out: false, + router_address: Some(Bytes::default()), + }; + + let encoder = EkuboSwapEncoder::new(String::default()); + + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + RECEIVER.to_string() + + concat!( + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + ), + ); + } + + #[test] + fn test_encode_swap_multi() { + let group_token_in = Bytes::from(Address::ZERO.as_slice()); + let group_token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let intermediary_token = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + + let encoder = EkuboSwapEncoder::new(String::default()); + + let encoding_context = EncodingContext { + receiver: RECEIVER.into(), + group_token_in: group_token_in.clone(), + group_token_out: group_token_out.clone(), + exact_out: false, + router_address: Some(Bytes::default()), + }; + + let first_swap = Swap { + component: ProtocolComponent { + static_attributes: HashMap::from([ + ("fee".to_string(), Bytes::from(0_u64)), + ("tick_spacing".to_string(), Bytes::from(0_u32)), + ( + "extension".to_string(), + Bytes::from("0x51d02a5948496a67827242eabc5725531342527c"), + ), // Oracle + ]), + ..Default::default() + }, + token_in: group_token_in.clone(), + token_out: intermediary_token.clone(), + split: 0f64, + }; + + let second_swap = Swap { + component: ProtocolComponent { + // 0.0025% fee & 0.005% base pool + static_attributes: HashMap::from([ + ("fee".to_string(), Bytes::from(461168601842738_u64)), + ("tick_spacing".to_string(), Bytes::from(50_u32)), + ("extension".to_string(), Bytes::zero(20)), + ]), + ..Default::default() + }, + token_in: intermediary_token.clone(), + token_out: group_token_out.clone(), + split: 0f64, + }; + + let first_encoded_swap = encoder + .encode_swap(first_swap, encoding_context.clone()) + .unwrap(); + + let second_encoded_swap = encoder + .encode_swap(second_swap, encoding_context) + .unwrap(); + + let combined_hex = + format!("{}{}", encode(first_encoded_swap), encode(second_encoded_swap)); + + println!("{}", combined_hex); + + assert_eq!( + combined_hex, + RECEIVER.to_string() + + concat!( + // group token in + "0000000000000000000000000000000000000000", + // token out 1st swap + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool config 1st swap + "51d02a5948496a67827242eabc5725531342527c000000000000000000000000", + // token out 2nd swap + "dac17f958d2ee523a2206206994597c13d831ec7", + // pool config 2nd swap + "00000000000000000000000000000000000000000001a36e2eb1c43200000032", + ), + ); + } + } } diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 183e215..85f541f 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use num_bigint::BigUint; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, @@ -34,7 +34,7 @@ impl Clone for EVMTychoEncoder { impl EVMTychoEncoder { pub fn new( - chain: tycho_core::models::Chain, + chain: tycho_common::models::Chain, strategy_encoder: Box, ) -> Result { let chain: Chain = Chain::from(chain); @@ -54,6 +54,8 @@ impl EVMTychoEncoder { /// swap's input is the chain's wrapped token. /// * If the solution is unwrapping, the checked token is the chain's native token and the last /// swap's output is the chain's wrapped token. + /// * The token cannot appear more than once in the solution unless it is the first and last + /// token (i.e. a true cyclical swap). fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { if solution.exact_out { return Err(EncodingError::FatalError( @@ -175,7 +177,7 @@ impl TychoEncoder for EVMTychoEncoder { mod tests { use std::str::FromStr; - use tycho_core::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; + use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; use super::*; use crate::encoding::{ @@ -247,7 +249,6 @@ mod tests { exact_out: false, given_amount: eth_amount_in.clone(), given_token: eth(), - router_address: Bytes::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap(), swaps: vec![swap], native_action: Some(NativeAction::Wrap), ..Default::default() diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index 96e04ea..b8af4f5 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -3,7 +3,7 @@ use std::{cmp::max, sync::Arc}; use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8}; use num_bigint::BigUint; use tokio::runtime::{Handle, Runtime}; -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, @@ -74,10 +74,10 @@ pub fn get_min_amount_for_solution(solution: Solution) -> BigUint { if let (Some(expected_amount), Some(slippage)) = (solution.expected_amount.as_ref(), solution.slippage) { - let one_hundred = BigUint::from(100u32); - let slippage_percent = BigUint::from((slippage * 100.0) as u32); - let multiplier = &one_hundred - slippage_percent; - let expected_amount_with_slippage = (expected_amount * &multiplier) / &one_hundred; + let bps = BigUint::from(10_000u32); + let slippage_percent = BigUint::from((slippage * 10000.0) as u32); + let multiplier = &bps - slippage_percent; + let expected_amount_with_slippage = (expected_amount * &multiplier) / &bps; min_amount_out = max(min_amount_out, expected_amount_with_slippage); } min_amount_out @@ -133,3 +133,27 @@ pub fn get_runtime() -> Result<(Handle, Option>), EncodingError> { } } } +#[cfg(test)] +mod tests { + use num_bigint::BigUint; + + use super::*; + use crate::encoding::models::Solution; + + #[test] + fn test_min_amount_out_small_slippage() { + // Tests that the calculation's precision is high enough to support a slippage of 0.1%. + + let solution = Solution { + exact_out: false, + given_amount: BigUint::from(1000000000000000000u64), + checked_amount: None, + slippage: Some(0.001f64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + ..Default::default() + }; + + let min_amount_out = get_min_amount_for_solution(solution); + assert_eq!(min_amount_out, BigUint::from(999000000000000000u64)); + } +} diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 0abc963..43af861 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -1,7 +1,7 @@ use hex; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; -use tycho_core::{ +use tycho_common::{ models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, Bytes, }; @@ -41,8 +41,6 @@ pub struct Solution { pub checked_amount: Option, /// List of swaps to fulfill the solution. pub swaps: Vec, - /// Address of the router contract to be used for the swaps. - pub router_address: Bytes, /// If set, the corresponding native action will be executed. pub native_action: Option, } @@ -104,14 +102,15 @@ pub struct Transaction { /// /// * `receiver`: Address of the receiver of the out token after the swaps are completed. /// * `exact_out`: true if the solution is a buy order, false if it is a sell order. -/// * `router_address`: Address of the router contract to be used for the swaps. +/// * `router_address`: Address of the router contract to be used for the swaps. Zero address if +/// solution does not require router address. /// * `group_token_in`: Token to be used as the input for the group swap. /// * `group_token_out`: Token to be used as the output for the group swap. #[derive(Clone, Debug)] pub struct EncodingContext { pub receiver: Bytes, pub exact_out: bool, - pub router_address: Bytes, + pub router_address: Option, pub group_token_in: Bytes, pub group_token_out: Bytes, } @@ -130,6 +129,7 @@ impl From for Chain { TychoCoreChain::Arbitrum => Chain { id: 42161, name: chain.to_string() }, TychoCoreChain::Starknet => Chain { id: 0, name: chain.to_string() }, TychoCoreChain::Base => Chain { id: 8453, name: chain.to_string() }, + TychoCoreChain::Unichain => Chain { id: 130, name: chain.to_string() }, } } } @@ -148,6 +148,7 @@ impl Chain { self.decode_hex("0000000000000000000000000000000000000000", decode_err_msg) } 324 => self.decode_hex("000000000000000000000000000000000000800A", decode_err_msg), + 130 => self.decode_hex("0000000000000000000000000000000000000000", decode_err_msg), _ => Err(EncodingError::InvalidInput(format!( "Native token not set for chain {:?}. Double check the chain is supported.", self.name @@ -162,6 +163,7 @@ impl Chain { 8453 => self.decode_hex("4200000000000000000000000000000000000006", decode_err_msg), 324 => self.decode_hex("5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", decode_err_msg), 42161 => self.decode_hex("82aF49447D8a07e3bd95BD0d56f35241523fBab1", decode_err_msg), + 130 => self.decode_hex("4200000000000000000000000000000000000006", decode_err_msg), _ => Err(EncodingError::InvalidInput(format!( "Wrapped token not set for chain {:?}. Double check the chain is supported.", self.name diff --git a/src/encoding/strategy_encoder.rs b/src/encoding/strategy_encoder.rs index b0e42fb..57630a0 100644 --- a/src/encoding/strategy_encoder.rs +++ b/src/encoding/strategy_encoder.rs @@ -1,4 +1,4 @@ -use tycho_core::Bytes; +use tycho_common::Bytes; use crate::encoding::{errors::EncodingError, models::Solution, swap_encoder::SwapEncoder};