diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md deleted file mode 100644 index c5d1df1..0000000 --- a/.github/ISSUE_TEMPLATE/task.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Task -about: A generic call forwarder related task -title: "" -labels: "" -assignees: "" ---- - -## Context - - - -## Scope - - - -## Definition of Done - - - -## Outcome - - diff --git a/.github/workflows/bindings.yml b/.github/workflows/crates.yml similarity index 68% rename from .github/workflows/bindings.yml rename to .github/workflows/crates.yml index 150eb0c..2e87417 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/crates.yml @@ -1,4 +1,4 @@ -name: Bindings +name: Crates on: push: @@ -8,10 +8,13 @@ on: jobs: tests: - name: Bindings Tests + name: Crates Tests runs-on: macos-latest env: ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + QUEUE_BASE_URL: ${{ secrets.QUEUE_BASE_URL }} + QUEUE_AUTH_TOKEN: ${{ secrets.QUEUE_AUTH_TOKEN }} + RISC0_SKIP_BUILD_KERNELS: true steps: - name: Checkout repository @@ -41,17 +44,17 @@ jobs: with: path: | target - key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + key: rust-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} restore-keys: rust- - name: Run rustfmt - run: just bindings-fmt-check + run: just crates-fmt-check - - name: Build Bindings - run: just bindings-build + - name: Build + run: just crates-build - - name: Lint Bindings - run: just bindings-lint + - name: Lint + run: just crates-lint - name: Run Tests - run: just bindings-test + run: just crates-test diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..977826c --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,29 @@ +# Generic Call Forwarder + +The forwarder contract and integration-test layer that lets ARM resources trigger +**arbitrary EVM calls** through the [Anoma EVM protocol adapter](https://github.com/anoma/pa-evm). + +## Language + +**Generic Call Forwarder**: +The EVM contract through which the protocol adapter executes a list of arbitrary +calls on behalf of an action. Use "generic call forwarder" for the contract. + +**Generic Call**: +A single `(to, value, data)` EVM call carried by a resource and executed by the +forwarder. A resource's label commits to the calls it authorizes. + +**Forwarder**: +The generic call forwarder contract (above) — the same forwarder role the protocol +adapter drives in other applications. + +## Note on upstream names + +`generic_call_library` and `generic_call_witness` (the resource logic, witness +types, and underlying circuit) live in `anoma/generic-call-resource` and keep those +names — they are immutable from this repo's perspective. + +The integration-test crate reuses the AnomaPay ERC20 wrap / transfer / unwrap +fixtures (via a dev-dependency on `anomapay-erc20-forwarder-integration-test`) to +move WETH into a shielded resource before driving a generic call; see that repo's +`CONTEXT.md` for those terms. diff --git a/Cargo.lock b/Cargo.lock index 3f38254..f809724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,7 +1633,7 @@ dependencies = [ "alloy 2.0.5", "alloy-chains", "anoma-generic-call-library", - "anoma-pa-evm-bindings", + "anoma-pa-evm-bindings 2.2.0", "dotenvy", "serde", "serde_json", @@ -1641,6 +1641,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "anoma-generic-call-forwarder-integration-test" +version = "0.2.0" +dependencies = [ + "alloy 2.0.5", + "alloy-chains", + "anoma-generic-call-forwarder-bindings", + "anoma-generic-call-library", + "anoma-generic-call-witness", + "anoma-pa-evm-integration-test", + "anoma-pa-testkit", + "anoma-rm-risc0", + "anomapay-erc20-forwarder-integration-test", + "anyhow", + "bincode", + "regex", + "risc0-zkvm", + "rstest", + "tokio", +] + [[package]] name = "anoma-generic-call-library" version = "1.0.0-rc.0" @@ -1681,6 +1702,68 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "anoma-pa-evm-bindings" +version = "3.0.0-rc.1" +source = "git+https://github.com/anoma/pa-evm?rev=95179723a8515337a90276d47374237c3e5a7784#95179723a8515337a90276d47374237c3e5a7784" +dependencies = [ + "alloy 2.0.5", + "alloy-chains", + "anoma-rm-risc0", + "dotenvy", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "anoma-pa-evm-integration-test" +version = "0.1.0" +source = "git+https://github.com/anoma/pa-evm?rev=95179723a8515337a90276d47374237c3e5a7784#95179723a8515337a90276d47374237c3e5a7784" +dependencies = [ + "alloy 2.0.5", + "alloy-chains", + "anoma-pa-evm-bindings 3.0.0-rc.1", + "anoma-pa-testkit", + "anoma-risc0-verifier-bindings", + "anoma-rm-risc0", + "anyhow", + "dotenvy", + "hex", + "risc0-zkvm", +] + +[[package]] +name = "anoma-pa-testkit" +version = "0.2.0" +source = "git+https://github.com/anoma/pa-testkit?rev=74cf6ad7cd488021128c34662e68171c7ae28e6d#74cf6ad7cd488021128c34662e68171c7ae28e6d" +dependencies = [ + "anoma-rm-risc0", + "anoma-rm-risc0-gadgets", + "anyhow", + "bincode", + "futures", + "heliax-ap-orchestrator-sdk", + "hex", + "k256", + "mockall", + "regex", + "risc0-zkvm", + "serde", + "sha2 0.11.0", + "tokio", +] + +[[package]] +name = "anoma-risc0-verifier-bindings" +version = "1.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9de2aea49f20548795e42423d7dd0ea32f3ec1c6737902aba0623e9033b2b5" +dependencies = [ + "alloy 2.0.5", + "serde", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" @@ -1718,6 +1801,44 @@ dependencies = [ "zeroize", ] +[[package]] +name = "anomapay-erc20-forwarder-bindings" +version = "3.0.0-rc.3" +source = "git+https://github.com/anoma/anomapay-erc20-forwarder?rev=ccdf93b869521261c8ebc2d05145abb782a8cc4d#ccdf93b869521261c8ebc2d05145abb782a8cc4d" +dependencies = [ + "alloy 2.0.5", + "alloy-chains", + "dotenvy", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "anomapay-erc20-forwarder-integration-test" +version = "0.2.0" +source = "git+https://github.com/anoma/anomapay-erc20-forwarder?rev=ccdf93b869521261c8ebc2d05145abb782a8cc4d#ccdf93b869521261c8ebc2d05145abb782a8cc4d" +dependencies = [ + "alloy 2.0.5", + "alloy-chains", + "anoma-pa-evm-integration-test", + "anoma-pa-testkit", + "anoma-rm-risc0", + "anoma-rm-risc0-gadgets", + "anomapay-erc20-forwarder-bindings", + "anyhow", + "bincode", + "risc0-zkvm", + "transfer_library", + "transfer_witness", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.102" @@ -2861,9 +2982,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "jobserver", @@ -2890,8 +3011,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -3338,7 +3461,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -3513,6 +3635,12 @@ dependencies = [ "clap", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -3851,6 +3979,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -3934,6 +4071,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -4202,6 +4345,40 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "heliax-ap-orchestrator-sdk" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56eef6564772c8be23bc7ebe41ae1ea1cab830a2873f30e27335fe57bf3916d5" +dependencies = [ + "heliax-ap-orchestrator-shared", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "heliax-ap-orchestrator-shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbdf274755868c87e16da5ec59e36c9bc037813cdc4844d8cc2f61be942825c" +dependencies = [ + "aes-gcm", + "base64", + "chrono", + "hex", + "pem", + "rand 0.8.6", + "ring", + "serde", + "serde_json", + "thiserror 2.0.18", + "utoipa", + "uuid", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -4795,6 +4972,7 @@ dependencies = [ "once_cell", "serdect", "sha2 0.10.9", + "signature", ] [[package]] @@ -5173,6 +5351,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mockall" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ndarray" version = "0.16.1" @@ -5654,6 +5858,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -5693,6 +5923,30 @@ dependencies = [ "toml_edit 0.25.12+spec-1.1.0", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -6132,6 +6386,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.28" @@ -6626,6 +6886,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.18.0" @@ -7205,6 +7494,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -7436,6 +7735,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "textwrap" version = "0.16.2" @@ -7493,12 +7798,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "fc1aa89044e7786ffb2ec017acb22cb7de5b0be46d0f21aea2b224b8561e5db2" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -7508,15 +7812,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "9d3bfe86347f0cc659f586f01e26303ccd32418f26f30c7b0309b3ca3a07d695" dependencies = [ "num-conv", "time-core", @@ -7556,7 +7860,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -7833,6 +8139,37 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "transfer_library" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c4d5d5b92eb32a8d9ea6a6672e7f6da6a763fefaf3a232880bad410a264c7" +dependencies = [ + "anoma-rm-risc0", + "anoma-rm-risc0-gadgets", + "hex", + "k256", + "lazy_static", + "serde", + "transfer_witness", +] + +[[package]] +name = "transfer_witness" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e70440e869b2b4a47e064e31a3d3116e7f9f76dd6e9e287a967e575bda09710" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "anoma-rm-risc0", + "anoma-rm-risc0-gadgets", + "bincode", + "k256", + "rand 0.9.4", + "serde", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -8039,13 +8376,40 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", + "uuid", +] + [[package]] name = "uuid" version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ + "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -8109,9 +8473,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -8792,18 +9156,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a21befb..a01e435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["bindings"] +members = ["crates/bindings", "crates/integration-test"] [workspace.package] keywords = ["anoma", "evm", "protocol-adapter", "forwarder"] @@ -14,11 +14,28 @@ edition = "2024" alloy = { version = "2.0.5", features = ["full", "eip712", "node-bindings"] } alloy-chains = "0.2.33" anoma-generic-call-library = "1.0.0-rc.0" +anoma-generic-call-witness = "1.0.0-rc.0" anoma-pa-evm-bindings = "2.2.0" +anoma-rm-risc0 = { version = "1.1.1", default-features = false } +anoma-rm-risc0-gadgets = "1.1.1" +anyhow = "1.0" +bincode = "1.3.3" dotenvy = "0.15.7" +regex = "1.12.3" +risc0-zkvm = "3.0.3" +rstest = "0.26.1" serde = { version = "1.0.228", default-features = false } serde_json = "1.0" thiserror = "2.0.18" tokio = { version = "1.52", features = ["rt-multi-thread"] } -anoma-generic-call-forwarder-bindings = { path = "bindings" } + +# Forwarder bindings (this repo) +anoma-generic-call-forwarder-bindings = { path = "crates/bindings" } + +# Upstream test crates. anoma-pa-testkit and pa-evm are pinned by git rev (see +# ADR-0002); the erc20 integration-test crate stays a local path until erc20 is +# pushed and pinned by git rev too. For cross-repo dev, override with `[patch]`. +anoma-pa-testkit = { git = "https://github.com/anoma/pa-testkit", rev = "74cf6ad7cd488021128c34662e68171c7ae28e6d" } +anoma-pa-evm-integration-test = { git = "https://github.com/anoma/pa-evm", rev = "95179723a8515337a90276d47374237c3e5a7784" } +anomapay-erc20-forwarder-integration-test = { git = "https://github.com/anoma/anomapay-erc20-forwarder", rev = "ccdf93b869521261c8ebc2d05145abb782a8cc4d" } diff --git a/README.md b/README.md index 63be1af..493d605 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Contracts Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml) [![soldeer.xyz](https://img.shields.io/badge/soldeer.xyz-anoma--generic--call--forwarder-blue?logo=ethereum)](https://soldeer.xyz/project/anoma--generic--call) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/generic-call-forwarder/refs/heads/main/contracts/LICENSE) +[![Contracts Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml) [![soldeer.xyz](https://img.shields.io/badge/soldeer.xyz-anoma--generic--call--forwarder-blue?logo=ethereum)](https://soldeer.xyz/project/anoma-generic-call-forwarder) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/generic-call-forwarder/refs/heads/main/contracts/LICENSE) -[![Bindings Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/bindings.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/bindings.yml) [![crates.io](https://img.shields.io/badge/crates.io-anoma--generic--call--forwarder--bindings-blue?logo=rust)](https://crates.io/crates/anoma-generic-call-forwarder-bindings) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/anoma-generic-call-forwarder/refs/heads/main/bindings/LICENSE) +[![Crates Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/crates.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/crates.yml) [![crates.io](https://img.shields.io/badge/crates.io-anoma--generic--call--forwarder--bindings-blue?logo=rust)](https://crates.io/crates/anoma-generic-call-forwarder-bindings) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/generic-call-forwarder/refs/heads/main/crates/bindings/LICENSE) # Generic Call Forwarder @@ -12,9 +12,10 @@ This monorepo is structured as follows: ``` . -├── audits -├── bindings ├── contracts +├── crates +│ ├── bindings +│ └── integration-test ├── Cargo.lock ├── Cargo.toml ├── README.md @@ -23,8 +24,10 @@ This monorepo is structured as follows: The [contracts](./contracts/) folder contains the contracts written in [Solidity](https://soliditylang.org/) as well as [Foundry forge](https://book.getfoundry.sh/forge/) tests and deploy scripts. -The [bindings](./bindings/) folder provides [Rust](https://www.rust-lang.org/) bindings for the conversion of Rust and [RISC Zero](https://risczero.com/) types into [EVM types](https://docs.soliditylang.org/en/latest/types.html) and exposes the deployment addresses on the different supported networks using the [alloy-rs](https://github.com/alloy-rs) -library. +The [crates](./crates/) folder contains the Rust workspace: + +- [bindings](./crates/bindings/) provides [Rust](https://www.rust-lang.org/) bindings for the forwarder contract and exposes its deployment addresses on the different supported networks using the [alloy-rs](https://github.com/alloy-rs) library. +- [integration-test](./crates/integration-test/) contains the Rust integration and e2e tests that deploy the forwarder against a local or forked chain and exercise generic EVM calls with risc0-proven transactions. ## Audits diff --git a/contracts/README.md b/contracts/README.md index f5a4ff4..6804821 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,4 +1,4 @@ -[![Contracts Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml) [![soldeer.xyz](https://img.shields.io/badge/soldeer.xyz-anoma--generic--call--forwarder-blue?logo=ethereum)](https://soldeer.xyz/project/anoma-generic-call-forwarder) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/generic-call-forwarder/refs/heads/main/bindings/LICENSE) +[![Contracts Tests](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml/badge.svg)](https://github.com/anoma/generic-call-forwarder/actions/workflows/contracts.yml) [![soldeer.xyz](https://img.shields.io/badge/soldeer.xyz-anoma--generic--call--forwarder-blue?logo=ethereum)](https://soldeer.xyz/project/anoma-generic-call-forwarder) [![License](https://img.shields.io/badge/license-MIT-blue)](https://raw.githubusercontent.com/anoma/generic-call-forwarder/refs/heads/main/contracts/LICENSE) # Generic Call Forwarder Contract diff --git a/contracts/soldeer.lock b/contracts/soldeer.lock deleted file mode 100644 index a6758bc..0000000 --- a/contracts/soldeer.lock +++ /dev/null @@ -1,41 +0,0 @@ -[[dependencies]] -name = "@openzeppelin-contracts" -version = "5.6.1" -url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_6_1_15-03-2026_09:19:50_contracts.zip" -checksum = "a3b6bc661be858c7c27f60a1708cbebe8c71034b4cc1e9fe270d0a05b069352f" -integrity = "bce03af7ada1eee21a7fff393f238bcd7cd75a022a4db55ffb6b0dbb32433d35" - -[[dependencies]] -name = "anoma-forwarder-bases" -version = "1.0.0-rc.3" -url = "https://soldeer-revisions.s3.amazonaws.com/anoma-forwarder-bases/1_0_0-rc_3_11-06-2026_11:53:46_contracts.zip" -checksum = "ef9802c435049e9139020f1a2ddc6decece9afcd689275e9bb0ff566a36ec1fd" -integrity = "2a27fa3b3a490ed45d75191fc561dff9d4eab202201d8e71dd3d76b5051b9f06" - -[[dependencies]] -name = "anomapay-erc20-forwarder" -version = "1.1.0-rc.2" -url = "https://soldeer-revisions.s3.amazonaws.com/anomapay-erc20-forwarder/1_1_0-rc_2_11-06-2026_13:35:35_contracts.zip" -checksum = "6a283016d4019116a814576796957aa8cf38be81cc9ce2057f9ca7a4a948dd90" -integrity = "a21f6ff5e5b010e6fb0661733de5c15142b9092b98e0d2e4935c35ce0053422e" - -[[dependencies]] -name = "forge-std" -version = "1.16.1" -url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_16_1_08-05-2026_08:51:16_forge-std-1.16.zip" -checksum = "839b61832925c7152c7b6dffbfa4998d9e606211179bd8f604733124e8a7cb57" -integrity = "60e55d10150354ca4a1e2985c5456c834b92b82ef85ab0e1d92a7786cddbd219" - -[[dependencies]] -name = "solady" -version = "0.1.26" -url = "https://soldeer-revisions.s3.amazonaws.com/solady/0_1_26_25-08-2025_15:30:06_solady.zip" -checksum = "9872ac7cfd32c1eba32800508a1325c49f4a4aa8c6f670454db91971a583e26b" -integrity = "5da4b5ca9cbad98812a4b75ad528ff34c72a0b84433204be6d1420c81de1d6ff" - -[[dependencies]] -name = "uniswap-permit2" -version = "0x000000000022D473030F116dDEE9F6B43aC78BA3" -url = "https://soldeer-revisions.s3.amazonaws.com/uniswap-permit2/0x000000000022D473030F116dDEE9F6B43aC78BA3_11-11-2024_08:00:23_permit2.zip" -checksum = "d4fb59a5d0b9bb91c6800b55a0fa4037dcf4d2a17e55750ffb369572a4dd5906" -integrity = "775a6885592b17bdc4556ca41127bf30c0d117264a6a494e37865f1ca28bcb0c" diff --git a/contracts/test/mocks/DexRouter.m.sol b/contracts/test/mocks/DexRouter.m.sol index 2b9eafe..d4afad6 100644 --- a/contracts/test/mocks/DexRouter.m.sol +++ b/contracts/test/mocks/DexRouter.m.sol @@ -12,6 +12,9 @@ import { ISignatureTransfer } from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/ISignatureTransfer.sol"; +/// @notice A minimal DEX router mock swapping `amountIn` of `tokenIn` for a fixed `amountOutMin` of `tokenOut`. +/// The variants differ only in how the input token is pulled from the caller: a classic ERC-20 allowance, +/// a Permit2 allowance, or a Permit2 signature. contract DexRouterMock { using SafeERC20 for IERC20; diff --git a/bindings/Cargo.toml b/crates/bindings/Cargo.toml similarity index 100% rename from bindings/Cargo.toml rename to crates/bindings/Cargo.toml diff --git a/bindings/LICENSE b/crates/bindings/LICENSE similarity index 100% rename from bindings/LICENSE rename to crates/bindings/LICENSE diff --git a/bindings/README.md b/crates/bindings/README.md similarity index 100% rename from bindings/README.md rename to crates/bindings/README.md diff --git a/bindings/deployments.json b/crates/bindings/deployments.json similarity index 100% rename from bindings/deployments.json rename to crates/bindings/deployments.json diff --git a/bindings/src/addresses.rs b/crates/bindings/src/addresses.rs similarity index 100% rename from bindings/src/addresses.rs rename to crates/bindings/src/addresses.rs diff --git a/bindings/src/contract.rs b/crates/bindings/src/contract.rs similarity index 100% rename from bindings/src/contract.rs rename to crates/bindings/src/contract.rs diff --git a/bindings/src/generated/generic_call_forwarder.rs b/crates/bindings/src/generated/generic_call_forwarder.rs similarity index 100% rename from bindings/src/generated/generic_call_forwarder.rs rename to crates/bindings/src/generated/generic_call_forwarder.rs diff --git a/bindings/src/generated/mod.rs b/crates/bindings/src/generated/mod.rs similarity index 100% rename from bindings/src/generated/mod.rs rename to crates/bindings/src/generated/mod.rs diff --git a/bindings/src/lib.rs b/crates/bindings/src/lib.rs similarity index 100% rename from bindings/src/lib.rs rename to crates/bindings/src/lib.rs diff --git a/bindings/tests/contract.rs b/crates/bindings/tests/contract.rs similarity index 100% rename from bindings/tests/contract.rs rename to crates/bindings/tests/contract.rs diff --git a/bindings/tests/deployments.rs b/crates/bindings/tests/deployments.rs similarity index 100% rename from bindings/tests/deployments.rs rename to crates/bindings/tests/deployments.rs diff --git a/crates/integration-test/Cargo.toml b/crates/integration-test/Cargo.toml new file mode 100644 index 0000000..ea0f95b --- /dev/null +++ b/crates/integration-test/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "anoma-generic-call-forwarder-integration-test" +description = "Integration and e2e tests for the Anoma generic call forwarder." +version = "0.2.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +publish = false + +[features] +default = [] +e2e = ["anoma-pa-evm-integration-test/e2e"] + +[dependencies] +anoma-pa-testkit = { workspace = true, features = ["fixtures"] } +# Normal dep (not dev) only because the `e2e` feature references it; the lib +# itself uses it solely via tests. +anoma-pa-evm-integration-test = { workspace = true } +anoma-generic-call-forwarder-bindings = { workspace = true } +anoma-generic-call-library = { workspace = true } +anoma-generic-call-witness = { workspace = true } +anoma-rm-risc0 = { workspace = true } + +alloy = { workspace = true, features = [ + "contract", + "json", + "providers", + "signer-local", + "sol-types", +] } +alloy-chains = { workspace = true } +anyhow = { workspace = true } +bincode = { workspace = true } +regex = { workspace = true } +risc0-zkvm = { workspace = true } + +[dev-dependencies] +anoma-pa-testkit = { workspace = true, features = ["mocks"] } +# generic-call only needs the AnomaPay ERC20 app *deployed* in its test setup. +anomapay-erc20-forwarder-integration-test = { workspace = true } +rstest = { workspace = true } +tokio = { workspace = true } diff --git a/crates/integration-test/artifacts/DexRouterMock.json b/crates/integration-test/artifacts/DexRouterMock.json new file mode 100644 index 0000000..ae85ce8 --- /dev/null +++ b/crates/integration-test/artifacts/DexRouterMock.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[{"name":"permit2","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"swapExactTokensForTokens","inputs":[{"name":"amountIn","type":"uint256","internalType":"uint256"},{"name":"amountOutMin","type":"uint256","internalType":"uint256"},{"name":"path","type":"address[]","internalType":"address[]"},{"name":"to","type":"address","internalType":"address"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"amountOut","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SafeERC20FailedOperation","inputs":[{"name":"token","type":"address","internalType":"address"}]}],"bytecode":{"object":"0x60a034606757601f61044f38819003918201601f19168301916001600160401b03831184841017606b57808492602094604052833981010312606757516001600160a01b038116908190036067576080526040516103cf908161008082396080518160d30152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f5f3560e01c6338ed173914610025575f80fd5b3461033f5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033f5760043560243560443567ffffffffffffffff811161033f573660238201121561033f57806004013567ffffffffffffffff811161033f573660248260051b8401011161033f576064359373ffffffffffffffffffffffffffffffffffffffff851680950361033f5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff821161037057821561034357610120602485016103a1565b91813b1561033f57608473ffffffffffffffffffffffffffffffffffffffff915f80948460405197889687957f36c785160000000000000000000000000000000000000000000000000000000087523360048801523060248801521660448601521660648401525af18015610334576102ea575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101908082116102bd5781101561029057602473ffffffffffffffffffffffffffffffffffffffff926101ed9260051b01016103a1565b1692604051927fa9059cbb0000000000000000000000000000000000000000000000000000000082526004528160245260208160448180885af16001825114811615610271575b836040521561024557602083838152f35b80847f5274afe70000000000000000000000000000000000000000000000000000000060249352600452fd5b600181151661028757843b15153d151616610234565b833d83823e3d90fd5b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526032600452fd5b6024867f4e487b710000000000000000000000000000000000000000000000000000000081526011600452fd5b90945067ffffffffffffffff8111610307576040525f935f610194565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040513d5f823e3d90fd5b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b507f6dfcc650000000000000000000000000000000000000000000000000000000005f5260a060045260245260445ffd5b3573ffffffffffffffffffffffffffffffffffffffff8116810361033f579056fea164736f6c6343000823000a","sourceMap":"445:628:26:-:0;;;;;;;;;;;;;-1:-1:-1;;445:628:26;;;;-1:-1:-1;;;;;445:628:26;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;445:628:26;;;;;;;;599:38;;445:628;;;;;;;;599:38;445:628;;;;;;;-1:-1:-1;445:628:26;;;;;;-1:-1:-1;445:628:26;;;;;-1:-1:-1;445:628:26","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f5f3560e01c6338ed173914610025575f80fd5b3461033f5760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033f5760043560243560443567ffffffffffffffff811161033f573660238201121561033f57806004013567ffffffffffffffff811161033f573660248260051b8401011161033f576064359373ffffffffffffffffffffffffffffffffffffffff851680950361033f5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff821161037057821561034357610120602485016103a1565b91813b1561033f57608473ffffffffffffffffffffffffffffffffffffffff915f80948460405197889687957f36c785160000000000000000000000000000000000000000000000000000000087523360048801523060248801521660448601521660648401525af18015610334576102ea575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101908082116102bd5781101561029057602473ffffffffffffffffffffffffffffffffffffffff926101ed9260051b01016103a1565b1692604051927fa9059cbb0000000000000000000000000000000000000000000000000000000082526004528160245260208160448180885af16001825114811615610271575b836040521561024557602083838152f35b80847f5274afe70000000000000000000000000000000000000000000000000000000060249352600452fd5b600181151661028757843b15153d151616610234565b833d83823e3d90fd5b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526032600452fd5b6024867f4e487b710000000000000000000000000000000000000000000000000000000081526011600452fd5b90945067ffffffffffffffff8111610307576040525f935f610194565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040513d5f823e3d90fd5b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b507f6dfcc650000000000000000000000000000000000000000000000000000000005f5260a060045260245260445ffd5b3573ffffffffffffffffffffffffffffffffffffffff8116810361033f579056fea164736f6c6343000823000a","sourceMap":"445:628:26:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;876:8;445:628;;7305:25:13;;7301:105;;445:628:26;;;;955:7;445:628;;;955:7;:::i;:::-;876:87;;;;;;445:628;;;;;;;;;876:87;;;;;445:628;876:87;;898:10;445:628;876:87;;445:628;918:4;445:628;;;;;;;;;;;;;;876:87;;;;;;;;445:628;;;;;;;;;;;;;;;;;;;1014:21;445:628;;;;;1014:21;:::i;:::-;445:628;1306:37:5;445:628:26;8544:1067:5;;8509:24;8544:1067;;445:628:26;8544:1067:5;;445:628:26;8544:1067:5;445:628:26;8544:1067:5;445:628:26;8544:1067:5;;;;;445:628:26;8544:1067:5;;;;;;;;445:628:26;8544:1067:5;445:628:26;8544:1067:5;1305:38;1301:116;;445:628:26;;;;;;1301:116:5;1366:40;;;445:628:26;1366:40:5;;445:628:26;;1366:40:5;8544:1067;445:628:26;8544:1067:5;;;;;;;;;;;;;;;;;;;;;;;;445:628:26;;;;;;;;;;;;;;;;;;;;876:87;445:628;;;;;;;;;;;876:87;;;;445:628;;;;;;;;;;876:87;445:628;;;;;;;;;876:87;445:628;;;;;;;;;;;;;7301:105:13;7353:42;;445:628:26;7353:42:13;445:628:26;;;;;;;7353:42:13;445:628:26;;;;;;;;;;:::o","linkReferences":{},"immutableReferences":{"4318":[{"start":211,"length":32}]}},"methodIdentifiers":{"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)":"38ed1739"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.35+commit.47b9dedd\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"permit2\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"bits\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"SafeCastOverflowedUintDowncast\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountOutMin\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"path\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"swapExactTokensForTokens\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amountOut\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"errors\":{\"SafeCastOverflowedUintDowncast(uint8,uint256)\":[{\"details\":\"Value doesn't fit in a uint of `bits` size.\"}],\"SafeERC20FailedOperation(address)\":[{\"details\":\"An operation with an ERC-20 token failed.\"}]},\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"test/mocks/DexRouter.m.sol\":\"DexRouterMock\"},\"evmVersion\":\"osaka\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":10000},\"remappings\":[\":@openzeppelin-contracts-5.6.1/=dependencies/@openzeppelin-contracts-5.6.1/\",\":anoma-forwarder-bases-1.0.0-rc.2/=dependencies/anoma-forwarder-bases-1.0.0-rc.2/\",\":anoma-pa-evm-1.2.0-rc.0/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/\",\":anomapay-erc20-forwarder-1.1.0-rc.1/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/\",\":elliptic-curve-solidity-0.2.5/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/dependencies/elliptic-curve-solidity-0.2.5/\",\":forge-std-1.16.1/=dependencies/forge-std-1.16.1/\",\":openzeppelin/contracts/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/dependencies/risc0-risc0-ethereum-3.0.1/lib/openzeppelin-contracts/contracts/\",\":solady-0.1.26/=dependencies/solady-0.1.26/\",\":solmate/=dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/lib/solmate/\",\":uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/=dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/\"],\"viaIR\":true},\"sources\":{\"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC1363.sol\":{\"keccak256\":\"0xd5ea07362ab630a6a3dee4285a74cf2377044ca2e4be472755ad64d7c5d4b69d\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://da5e832b40fc5c3145d3781e2e5fa60ac2052c9d08af7e300dc8ab80c4343100\",\"dweb:/ipfs/QmTzf7N5ZUdh5raqtzbM11yexiUoLC9z3Ws632MCuycq1d\"]},\"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC165.sol\":{\"keccak256\":\"0x0afcb7e740d1537b252cb2676f600465ce6938398569f09ba1b9ca240dde2dfc\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://1c299900ac4ec268d4570ecef0d697a3013cd11a6eb74e295ee3fbc945056037\",\"dweb:/ipfs/Qmab9owJoxcA7vJT5XNayCMaUR1qxqj1NDzzisduwaJMcZ\"]},\"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC20.sol\":{\"keccak256\":\"0x1a6221315ce0307746c2c4827c125d821ee796c74a676787762f4778671d4f44\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://1bb2332a7ee26dd0b0de9b7fe266749f54820c99ab6a3bcb6f7e6b751d47ee2d\",\"dweb:/ipfs/QmcRWpaBeCYkhy68PR3B4AgD7asuQk7PwkWxrvJbZcikLF\"]},\"dependencies/@openzeppelin-contracts-5.6.1/token/ERC20/IERC20.sol\":{\"keccak256\":\"0x74ed01eb66b923d0d0cfe3be84604ac04b76482a55f9dd655e1ef4d367f95bc2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://5282825a626cfe924e504274b864a652b0023591fa66f06a067b25b51ba9b303\",\"dweb:/ipfs/QmeCfPykghhMc81VJTrHTC7sF6CRvaA1FXVq2pJhwYp1dV\"]},\"dependencies/@openzeppelin-contracts-5.6.1/token/ERC20/utils/SafeERC20.sol\":{\"keccak256\":\"0x304d732678032a9781ae85c8f204c8fba3d3a5e31c02616964e75cfdc5049098\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://299ced486011781dc98f638059678323c03079fefae1482abaa2135b22fa92d0\",\"dweb:/ipfs/QmbZNbcPTBxNvwChavN2kkZZs7xHhYL7mv51KrxMhsMs3j\"]},\"dependencies/@openzeppelin-contracts-5.6.1/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617\",\"dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u\"]},\"dependencies/@openzeppelin-contracts-5.6.1/utils/math/SafeCast.sol\":{\"keccak256\":\"0xc8cae21c9ae4a46e5162ff9bf5b351d6fa6a6eba72d515f3bc1bdfeda7fdf083\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ce830ebcf28e31643caba318996db3763c36d52cd0f23798ba83c135355d45e9\",\"dweb:/ipfs/QmdGPcvptHN7UBCbUYBbRX3hiRVRFLRwno8b4uga6uFNif\"]},\"dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IAllowanceTransfer.sol\":{\"keccak256\":\"0x37f0ac203b6ef605c9533e1a739477e8e9dcea90710b40e645a367f8a21ace29\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://e0104d72aeaec1cd66cc232e7de7b7ead08608efcc179491b8a66387614670b0\",\"dweb:/ipfs/QmfAZDyuNC9FXXbnJUwqHNwmAK6uRrXxtWEytLsxjskPsN\"]},\"dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IEIP712.sol\":{\"keccak256\":\"0xfdccf2b9639070803cd0e4198427fb0df3cc452ca59bd3b8a0d957a9a4254138\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f7c936ac42ce89e827db905a1544397f8bdf46db34cdb6aa1b90dea42fdb4c72\",\"dweb:/ipfs/QmVgurxo1N31qZqkPBirw9Z7S9tLYmv6jSwQp8R8ur2cBk\"]},\"test/mocks/DexRouter.m.sol\":{\"keccak256\":\"0x92676b354203eb284569cfd899bf25ae0f51e35de73951f6d17f9399232655f5\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://875b795e537972d97a809725f33646184a2a9f4405e5d5c3ec8389ac17c36731\",\"dweb:/ipfs/QmUwibsRfsnvjtdWUFmBYZk3NZDDgEeXnM9qDoyK3KBNcK\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.35+commit.47b9dedd"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"permit2","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint8","name":"bits","type":"uint8"},{"internalType":"uint256","name":"value","type":"uint256"}],"type":"error","name":"SafeCastOverflowedUintDowncast"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"type":"error","name":"SafeERC20FailedOperation"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin-contracts-5.6.1/=dependencies/@openzeppelin-contracts-5.6.1/","anoma-forwarder-bases-1.0.0-rc.2/=dependencies/anoma-forwarder-bases-1.0.0-rc.2/","anoma-pa-evm-1.2.0-rc.0/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/","anomapay-erc20-forwarder-1.1.0-rc.1/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/","elliptic-curve-solidity-0.2.5/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/dependencies/elliptic-curve-solidity-0.2.5/","forge-std-1.16.1/=dependencies/forge-std-1.16.1/","openzeppelin/contracts/=dependencies/anomapay-erc20-forwarder-1.1.0-rc.1/dependencies/anoma-pa-evm-1.2.0-rc.0/dependencies/risc0-risc0-ethereum-3.0.1/lib/openzeppelin-contracts/contracts/","solady-0.1.26/=dependencies/solady-0.1.26/","solmate/=dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/lib/solmate/","uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/=dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/"],"optimizer":{"enabled":true,"runs":10000},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"test/mocks/DexRouter.m.sol":"DexRouterMock"},"evmVersion":"osaka","libraries":{},"viaIR":true},"sources":{"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC1363.sol":{"keccak256":"0xd5ea07362ab630a6a3dee4285a74cf2377044ca2e4be472755ad64d7c5d4b69d","urls":["bzz-raw://da5e832b40fc5c3145d3781e2e5fa60ac2052c9d08af7e300dc8ab80c4343100","dweb:/ipfs/QmTzf7N5ZUdh5raqtzbM11yexiUoLC9z3Ws632MCuycq1d"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC165.sol":{"keccak256":"0x0afcb7e740d1537b252cb2676f600465ce6938398569f09ba1b9ca240dde2dfc","urls":["bzz-raw://1c299900ac4ec268d4570ecef0d697a3013cd11a6eb74e295ee3fbc945056037","dweb:/ipfs/Qmab9owJoxcA7vJT5XNayCMaUR1qxqj1NDzzisduwaJMcZ"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/interfaces/IERC20.sol":{"keccak256":"0x1a6221315ce0307746c2c4827c125d821ee796c74a676787762f4778671d4f44","urls":["bzz-raw://1bb2332a7ee26dd0b0de9b7fe266749f54820c99ab6a3bcb6f7e6b751d47ee2d","dweb:/ipfs/QmcRWpaBeCYkhy68PR3B4AgD7asuQk7PwkWxrvJbZcikLF"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/token/ERC20/IERC20.sol":{"keccak256":"0x74ed01eb66b923d0d0cfe3be84604ac04b76482a55f9dd655e1ef4d367f95bc2","urls":["bzz-raw://5282825a626cfe924e504274b864a652b0023591fa66f06a067b25b51ba9b303","dweb:/ipfs/QmeCfPykghhMc81VJTrHTC7sF6CRvaA1FXVq2pJhwYp1dV"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/token/ERC20/utils/SafeERC20.sol":{"keccak256":"0x304d732678032a9781ae85c8f204c8fba3d3a5e31c02616964e75cfdc5049098","urls":["bzz-raw://299ced486011781dc98f638059678323c03079fefae1482abaa2135b22fa92d0","dweb:/ipfs/QmbZNbcPTBxNvwChavN2kkZZs7xHhYL7mv51KrxMhsMs3j"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/utils/introspection/IERC165.sol":{"keccak256":"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c","urls":["bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617","dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u"],"license":"MIT"},"dependencies/@openzeppelin-contracts-5.6.1/utils/math/SafeCast.sol":{"keccak256":"0xc8cae21c9ae4a46e5162ff9bf5b351d6fa6a6eba72d515f3bc1bdfeda7fdf083","urls":["bzz-raw://ce830ebcf28e31643caba318996db3763c36d52cd0f23798ba83c135355d45e9","dweb:/ipfs/QmdGPcvptHN7UBCbUYBbRX3hiRVRFLRwno8b4uga6uFNif"],"license":"MIT"},"dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IAllowanceTransfer.sol":{"keccak256":"0x37f0ac203b6ef605c9533e1a739477e8e9dcea90710b40e645a367f8a21ace29","urls":["bzz-raw://e0104d72aeaec1cd66cc232e7de7b7ead08608efcc179491b8a66387614670b0","dweb:/ipfs/QmfAZDyuNC9FXXbnJUwqHNwmAK6uRrXxtWEytLsxjskPsN"],"license":"MIT"},"dependencies/uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IEIP712.sol":{"keccak256":"0xfdccf2b9639070803cd0e4198427fb0df3cc452ca59bd3b8a0d957a9a4254138","urls":["bzz-raw://f7c936ac42ce89e827db905a1544397f8bdf46db34cdb6aa1b90dea42fdb4c72","dweb:/ipfs/QmVgurxo1N31qZqkPBirw9Z7S9tLYmv6jSwQp8R8ur2cBk"],"license":"MIT"},"test/mocks/DexRouter.m.sol":{"keccak256":"0x92676b354203eb284569cfd899bf25ae0f51e35de73951f6d17f9399232655f5","urls":["bzz-raw://875b795e537972d97a809725f33646184a2a9f4405e5d5c3ec8389ac17c36731","dweb:/ipfs/QmUwibsRfsnvjtdWUFmBYZk3NZDDgEeXnM9qDoyK3KBNcK"],"license":"MIT"}},"version":1},"id":26} \ No newline at end of file diff --git a/crates/integration-test/src/deploy/dex_router.rs b/crates/integration-test/src/deploy/dex_router.rs new file mode 100644 index 0000000..af96d2e --- /dev/null +++ b/crates/integration-test/src/deploy/dex_router.rs @@ -0,0 +1,26 @@ +use alloy::primitives::Address; +use alloy::providers::Provider; +use alloy::sol; +use anyhow::Context; + +sol!( + #[allow(missing_docs)] + #[derive(Debug)] + #[sol(rpc)] + DexRouterMock, + "artifacts/DexRouterMock.json" +); + +/// Deploys the mock Uniswap-style DEX router used to demo swapping one ERC20 for +/// another from inside the generic-call forwarder. It pulls the input token from +/// the caller via Permit2 and pays out a fixed `amountOutMin` of the output token. +pub async fn deploy_dex_router_mock

(provider: P, permit2: Address) -> anyhow::Result

+where + P: Provider + Clone, +{ + let deployed = DexRouterMock::deploy(provider, permit2) + .await + .context("failed to deploy DexRouterMock")?; + + Ok(*deployed.address()) +} diff --git a/crates/integration-test/src/deploy/forwarder.rs b/crates/integration-test/src/deploy/forwarder.rs new file mode 100644 index 0000000..7326624 --- /dev/null +++ b/crates/integration-test/src/deploy/forwarder.rs @@ -0,0 +1,49 @@ +use alloy::primitives::Address; +use alloy::primitives::B256; +use alloy::providers::Provider; +use anoma_generic_call_forwarder_bindings::generated::generic_call_forwarder::GenericCallForwarder; +use anyhow::Context; + +use crate::state::addresses::insert_generic_call_forwarder_v1_address; + +#[inline] +pub fn generic_call_forwarder

( + address: Address, + provider: P, +) -> GenericCallForwarder::GenericCallForwarderInstance

+where + P: Provider, +{ + GenericCallForwarder::GenericCallForwarderInstance::new(address, provider) +} + +pub async fn deploy_generic_call_forwarder

( + provider: P, + protocol_adapter: Address, + logic_ref: B256, +) -> anyhow::Result

+where + P: Provider + Clone, +{ + let deployed = GenericCallForwarder::deploy(provider, protocol_adapter, logic_ref) + .await + .context("failed to deploy GenericCallForwarder")?; + + Ok(*deployed.address()) +} + +pub async fn deploy_and_insert_generic_call_forwarder

( + builder: &mut anoma_pa_testkit::environment::StateBuilder, + provider: P, + protocol_adapter: Address, + logic_ref: B256, +) -> anyhow::Result

+where + P: Provider + Clone, +{ + let address = deploy_generic_call_forwarder(provider, protocol_adapter, logic_ref).await?; + + insert_generic_call_forwarder_v1_address(builder, address); + + Ok(address) +} diff --git a/crates/integration-test/src/deploy/mod.rs b/crates/integration-test/src/deploy/mod.rs new file mode 100644 index 0000000..13895d6 --- /dev/null +++ b/crates/integration-test/src/deploy/mod.rs @@ -0,0 +1,4 @@ +//! Generic call forwarder deploy bindings. + +pub mod dex_router; +pub mod forwarder; diff --git a/crates/integration-test/src/fixtures/generic_call/action.rs b/crates/integration-test/src/fixtures/generic_call/action.rs new file mode 100644 index 0000000..d0eff39 --- /dev/null +++ b/crates/integration-test/src/fixtures/generic_call/action.rs @@ -0,0 +1,90 @@ +use anoma_generic_call_library::GenericCall; +use anoma_generic_call_library::GenericCallLogic; +use anoma_pa_testkit::witness::ActionWitnesses; +use anoma_rm_risc0::action_tree::MerkleTree as ArmTree; +use anoma_rm_risc0::compliance::ComplianceWitness; +use anoma_rm_risc0::resource::Resource; +use anyhow::Context; + +use super::resource; +use super::resource::Overrides; +use crate::logic; + +/// The derived data of a built generic-call action: the action witnesses +/// plus the ephemeral resources it consumes and creates. +pub struct ActionData { + pub witnesses: ActionWitnesses, + pub consumed_ephemeral: Resource, + pub created_ephemeral: Resource, +} + +/// Build a generic-call action: consumes an ephemeral resource whose label +/// commits to the forwarder `calls` and creates an ephemeral resource carrying +/// no calls. +pub fn build( + seed: u8, + forwarder_addr: Vec, + calls: Vec, + overrides: Overrides, +) -> anyhow::Result { + let created_calls: Vec = Vec::new(); + + let nf_key = resource::nullifier_key(seed); + let nk_commitment = nf_key.commit(); + + let consumed_ephemeral = + resource::consumed(seed, nk_commitment, &forwarder_addr, &calls, &overrides)?; + let consumed_nullifier = consumed_ephemeral + .nullifier(&nf_key) + .context("failed to compute consumed nullifier")?; + let created_ephemeral = resource::created( + seed, + nk_commitment, + consumed_nullifier, + &forwarder_addr, + &created_calls, + &overrides, + )?; + + let compliance_witness = ComplianceWitness::from_resources( + consumed_ephemeral, + *anoma_rm_risc0::compliance::INITIAL_ROOT, + nf_key.clone(), + created_ephemeral, + ); + + let action_tree_root = ArmTree::new(vec![consumed_nullifier, created_ephemeral.commitment()]) + .root() + .context("failed to compute action tree root")?; + + let consumed_logic_witness = GenericCallLogic::consumed_ephemeral_resource_logic( + consumed_ephemeral, + action_tree_root, + nf_key, + forwarder_addr.clone(), + calls.clone(), + ) + .witness; + + let created_logic_witness = GenericCallLogic::created_ephemeral_resource_logic( + created_ephemeral, + action_tree_root, + forwarder_addr, + created_calls, + ) + .witness; + + let witnesses = ActionWitnesses { + compliance_witnesses: vec![Box::new(compliance_witness)], + logic_witnesses: vec![ + Box::new(logic::Witness::new(consumed_logic_witness)), + Box::new(logic::Witness::new(created_logic_witness)), + ], + }; + + Ok(ActionData { + witnesses, + consumed_ephemeral, + created_ephemeral, + }) +} diff --git a/crates/integration-test/src/fixtures/generic_call/mod.rs b/crates/integration-test/src/fixtures/generic_call/mod.rs new file mode 100644 index 0000000..4d8fded --- /dev/null +++ b/crates/integration-test/src/fixtures/generic_call/mod.rs @@ -0,0 +1,9 @@ +//! Generic-call action: consumes an ephemeral resource carrying the forwarder +//! calls and creates an ephemeral resource, driving arbitrary calls through the +//! generic-call forwarder. + +mod action; +mod resource; + +pub use action::{ActionData, build}; +pub use resource::Overrides; diff --git a/crates/integration-test/src/fixtures/generic_call/resource.rs b/crates/integration-test/src/fixtures/generic_call/resource.rs new file mode 100644 index 0000000..e22401b --- /dev/null +++ b/crates/integration-test/src/fixtures/generic_call/resource.rs @@ -0,0 +1,118 @@ +use anoma_generic_call_witness::GenericCall; +use anoma_generic_call_witness::calculate_label_ref; +use anoma_generic_call_witness::calculate_value_ref; +use anoma_generic_call_witness::encode_generic_call_forwarder_input; +use anoma_rm_risc0::Digest; +use anoma_rm_risc0::nullifier_key::NullifierKey; +use anoma_rm_risc0::nullifier_key::NullifierKeyCommitment; +use anoma_rm_risc0::resource::Resource; +use anyhow::Context; + +use crate::logic; + +#[derive(Clone, Debug, Default)] +pub struct Overrides { + pub consumed_quantity: Option, + pub created_quantity: Option, + pub consumed_value_ref: Option, + pub created_value_ref: Option, + pub consumed_label_ref: Option, + pub created_label_ref: Option, + pub consumed_is_ephemeral: Option, + pub created_is_ephemeral: Option, + pub consumed_nonce: Option<[u8; 32]>, + pub created_nonce: Option<[u8; 32]>, +} + +impl Overrides { + pub fn invalid_consumed_non_ephemeral() -> Self { + Self { + consumed_is_ephemeral: Some(false), + ..Self::default() + } + } + + pub fn invalid_created_non_ephemeral() -> Self { + Self { + created_is_ephemeral: Some(false), + ..Self::default() + } + } + + pub fn invalid_consumed_label_ref() -> Self { + Self { + consumed_label_ref: Some(Digest::default()), + ..Self::default() + } + } +} + +pub(super) fn consumed( + seed: u8, + nk_commitment: NullifierKeyCommitment, + forwarder_addr: &[u8], + calls: &[GenericCall], + overrides: &Overrides, +) -> anyhow::Result { + let encoded_calls = + encode_generic_call_forwarder_input(calls).context("failed to encode generic calls")?; + + let value_ref = overrides + .consumed_value_ref + .unwrap_or(calculate_value_ref(&encoded_calls)); + + let label_ref = overrides + .consumed_label_ref + .unwrap_or(calculate_label_ref(forwarder_addr)); + + Ok(Resource { + logic_ref: logic::verifying_key(), + label_ref, + quantity: overrides.consumed_quantity.unwrap_or(0), + value_ref, + is_ephemeral: overrides.consumed_is_ephemeral.unwrap_or(true), + nonce: overrides.consumed_nonce.unwrap_or([seed; 32]), + nk_commitment, + rand_seed: [seed.wrapping_add(11); 32], + }) +} + +pub(super) fn created( + seed: u8, + nk_commitment: NullifierKeyCommitment, + consumed_nullifier: Digest, + forwarder_addr: &[u8], + calls: &[GenericCall], + overrides: &Overrides, +) -> anyhow::Result { + let encoded_calls = + encode_generic_call_forwarder_input(calls).context("failed to encode generic calls")?; + + let value_ref = overrides + .created_value_ref + .unwrap_or(calculate_value_ref(&encoded_calls)); + + let label_ref = overrides + .created_label_ref + .unwrap_or(calculate_label_ref(forwarder_addr)); + + let default_nonce: [u8; 32] = consumed_nullifier + .as_bytes() + .try_into() + .context("nullifier must be 32 bytes")?; + + Ok(Resource { + logic_ref: logic::verifying_key(), + label_ref, + quantity: overrides.created_quantity.unwrap_or(0), + value_ref, + is_ephemeral: overrides.created_is_ephemeral.unwrap_or(true), + nonce: overrides.created_nonce.unwrap_or(default_nonce), + nk_commitment, + rand_seed: [seed.wrapping_add(33); 32], + }) +} + +pub(super) fn nullifier_key(seed: u8) -> NullifierKey { + NullifierKey::from_bytes([seed; 32]) +} diff --git a/crates/integration-test/src/fixtures/mod.rs b/crates/integration-test/src/fixtures/mod.rs new file mode 100644 index 0000000..c67f6c2 --- /dev/null +++ b/crates/integration-test/src/fixtures/mod.rs @@ -0,0 +1,7 @@ +//! Generic-call action fixtures. The single [`generic_call`] kind exposes the +//! single-builder surface of testkit ADR-0003 — `build`, the derived-data +//! bundle `ActionData`, and `Overrides` with named `invalid_*` variants for negative +//! tests. The shared resource logic (verifying key + witness adapter) lives in +//! [`crate::logic`]. + +pub mod generic_call; diff --git a/crates/integration-test/src/lib.rs b/crates/integration-test/src/lib.rs new file mode 100644 index 0000000..b221e6e --- /dev/null +++ b/crates/integration-test/src/lib.rs @@ -0,0 +1,10 @@ +//! Integration-test harness for the Anoma generic call forwarder. +//! +//! Exposes the generic-call provisioning helpers (`deploy`, `state`), the +//! resource `logic` (verifying key + witness adapter), and the action `fixtures`. +//! Scenario setups and the scenarios live under `tests/`. + +pub mod deploy; +pub mod fixtures; +pub mod logic; +pub mod state; diff --git a/crates/integration-test/src/logic.rs b/crates/integration-test/src/logic.rs new file mode 100644 index 0000000..c70d17a --- /dev/null +++ b/crates/integration-test/src/logic.rs @@ -0,0 +1,51 @@ +//! The generic-call resource logic: its verifying key and the [`LogicWitness`] +//! adapter over [`GenericCallWitness`] that the generic-call action feeds to the +//! prover. + +use anoma_generic_call_library::GenericCallLogic; +use anoma_generic_call_witness::GenericCallWitness; +use anoma_pa_testkit::witness::LogicWitness; +use anoma_rm_risc0::Digest; +use anoma_rm_risc0::logic_instance::LogicInstance; +use anoma_rm_risc0::logic_proof::LogicProver; +use anoma_rm_risc0::resource_logic::LogicCircuit; +use anyhow::Context; + +/// Verifying key (image id) of the generic-call resource logic. +#[inline] +pub fn verifying_key() -> Digest { + GenericCallLogic::verifying_key() +} + +/// Adapts a [`GenericCallWitness`] to the testkit's [`LogicWitness`] trait. +pub(crate) struct Witness { + inner: GenericCallWitness, +} + +impl Witness { + #[inline] + pub(crate) fn new(inner: GenericCallWitness) -> Self { + Self { inner } + } +} + +impl LogicWitness for Witness { + fn verifying_key(&self) -> Digest { + verifying_key() + } + + fn constrain(&self) -> anyhow::Result { + LogicCircuit::constrain(&self.inner) + .map_err(anyhow::Error::from) + .context("invalid generic call logic witness") + } + + fn witness_to_vec(&self) -> anyhow::Result> { + risc0_zkvm::serde::to_vec(&self.inner) + .context("failed to serialize generic call logic witness to risc0 words") + } + + fn proving_key(&self) -> Vec { + GenericCallLogic::proving_key().to_vec() + } +} diff --git a/crates/integration-test/src/state/addresses.rs b/crates/integration-test/src/state/addresses.rs new file mode 100644 index 0000000..4f309b3 --- /dev/null +++ b/crates/integration-test/src/state/addresses.rs @@ -0,0 +1,69 @@ +use alloy::primitives::Address; +use anoma_pa_testkit::environment::Environment; +use anoma_pa_testkit::environment::State; +use anoma_pa_testkit::environment::StateBuilder; +use anyhow::Context; + +use super::keys::generic_call_forwarder_v1_addr_key; + +#[inline] +pub fn insert_generic_call_forwarder_v1_address(builder: &mut StateBuilder, address: Address) { + builder.insert(generic_call_forwarder_v1_addr_key(), address); +} + +#[inline] +pub fn generic_call_forwarder_v1_address(env: &E) -> anyhow::Result
+where + E: Environment, +{ + generic_call_forwarder_v1_address_in_state(env.state()) +} + +#[inline] +pub fn generic_call_forwarder_v1_address_in_state(state: &State) -> anyhow::Result
{ + state + .get::
(generic_call_forwarder_v1_addr_key()) + .copied() + .context("failed to retrieve generic call forwarder v1 address from env") +} + +#[cfg(test)] +mod tests { + use anoma_pa_testkit::mocks::MockEnvironment; + + use super::*; + + #[test] + fn insert_and_resolve_generic_call_forwarder_v1_address() { + let expected = Address::from([0x41; 20]); + + let state = { + let mut builder = StateBuilder::new(); + insert_generic_call_forwarder_v1_address(&mut builder, expected); + builder.finalize() + }; + + let mut env = MockEnvironment::new(); + env.expect_state().return_const(state); + + let resolved = generic_call_forwarder_v1_address(&env) + .expect("must resolve stored generic call forwarder address"); + assert_eq!(resolved, expected); + } + + #[test] + fn missing_generic_call_forwarder_v1_address_errors() { + let state = StateBuilder::new().finalize(); + + let mut env = MockEnvironment::new(); + env.expect_state().return_const(state); + + let err = generic_call_forwarder_v1_address(&env) + .expect_err("must fail on missing generic call forwarder address key"); + assert!( + err.to_string() + .contains("failed to retrieve generic call forwarder v1 address from env"), + "error should mention missing generic call forwarder address, got: {err}" + ); + } +} diff --git a/crates/integration-test/src/state/keys.rs b/crates/integration-test/src/state/keys.rs new file mode 100644 index 0000000..27d5461 --- /dev/null +++ b/crates/integration-test/src/state/keys.rs @@ -0,0 +1,19 @@ +pub const KEY_GENERIC_CALL_FORWARDER_V1_ADDRESS: &str = "evm.forwarder.generic-call.v1"; + +#[inline] +pub fn generic_call_forwarder_v1_addr_key() -> &'static str { + KEY_GENERIC_CALL_FORWARDER_V1_ADDRESS +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generic_call_forwarder_v1_key_format() { + assert_eq!( + generic_call_forwarder_v1_addr_key(), + "evm.forwarder.generic-call.v1" + ); + } +} diff --git a/crates/integration-test/src/state/mod.rs b/crates/integration-test/src/state/mod.rs new file mode 100644 index 0000000..9ed0f39 --- /dev/null +++ b/crates/integration-test/src/state/mod.rs @@ -0,0 +1,2 @@ +pub mod addresses; +pub mod keys; diff --git a/crates/integration-test/tests/common/mod.rs b/crates/integration-test/tests/common/mod.rs new file mode 100644 index 0000000..156cf11 --- /dev/null +++ b/crates/integration-test/tests/common/mod.rs @@ -0,0 +1,102 @@ +//! Shared scenario setups for the generic-call integration tests. +//! +//! This lives under `tests/` so it is purely test code: it is never compiled +//! into a library and is invisible to dependents. Each integration test +//! includes it with `mod common;`. Reusable helpers (assertions, provisioning) +//! live in the testkit and the integration-test crates instead. + +#[cfg(feature = "e2e")] +pub type EvmE2eEnv = anoma_pa_evm_integration_test::envs::e2e::Environment; + +pub type EvmLocalEnv = anoma_pa_evm_integration_test::envs::local::Environment; + +use alloy::primitives::{B256, U256}; +use anoma_generic_call_forwarder_integration_test::deploy::forwarder::deploy_and_insert_generic_call_forwarder; +use anoma_generic_call_forwarder_integration_test::logic as generic_call_logic; +use anoma_pa_evm_integration_test::keychain::EvmSigner; +use anoma_pa_evm_integration_test::state::actors::default_signer_in_state; +use anoma_pa_evm_integration_test::state::pa::pa_address_in_state; +use anoma_pa_testkit::environment::StateBuilder; +use anoma_pa_testkit::fixtures::identities; +use anomapay_erc20_forwarder_integration_test::deploy::erc20::ierc20_bindings::ierc20; +use anomapay_erc20_forwarder_integration_test::deploy::erc20::weth_bindings::deploy_and_insert_weth; +use anomapay_erc20_forwarder_integration_test::deploy::forwarder::deploy_and_insert_erc20_forwarder; +use anomapay_erc20_forwarder_integration_test::deploy::permit2::PERMIT2_CANONICAL_ADDRESS; +use anomapay_erc20_forwarder_integration_test::deploy::permit2::deploy_permit2_canonical_from_state; +use anomapay_erc20_forwarder_integration_test::logic as erc20_logic; +use anyhow::Context; + +pub use anoma_pa_testkit::{commitment_root, execute_tx, prove_actions}; + +pub async fn setup_transfer_generic_call_local_env() -> anyhow::Result { + EvmLocalEnv::setup(async |builder: &mut StateBuilder| { + deploy_permit2_canonical_from_state(builder.as_state()) + .await + .context("failed to deploy Permit2 at canonical address")?; + setup_transfer_generic_call_env_on_builder(builder).await + }) + .await +} + +#[cfg(feature = "e2e")] +pub async fn setup_transfer_generic_call_e2e_env() -> anyhow::Result { + EvmE2eEnv::setup(async |builder: &mut StateBuilder| { + setup_transfer_generic_call_env_on_builder(builder).await + }) + .await +} + +async fn setup_transfer_generic_call_env_on_builder( + builder: &mut StateBuilder, +) -> anyhow::Result<()> { + let provider = default_signer_in_state(builder.as_state()) + .context("failed to retrieve default signer from setup state")?; + let pa_address = pa_address_in_state(builder.as_state()) + .context("failed to retrieve protocol adapter address from setup state")?; + + let deployer = identities::alice() + .context("failed to build sender keychain")? + .address(); + + let token = deploy_and_insert_weth( + builder, + "weth", + provider.clone(), + deployer, + U256::from(1_000_000u64), + ) + .await + .context("failed to deploy and insert WETH9")?; + + let transfer_logic_ref = B256::from(<[u8; 32]>::from(erc20_logic::verifying_key())); + deploy_and_insert_erc20_forwarder( + builder, + provider.clone(), + pa_address, + transfer_logic_ref, + deployer, + ) + .await + .context("failed to deploy and insert ERC20 forwarder v1")?; + + let generic_call_logic_ref = B256::from(<[u8; 32]>::from(generic_call_logic::verifying_key())); + deploy_and_insert_generic_call_forwarder( + builder, + provider.clone(), + pa_address, + generic_call_logic_ref, + ) + .await + .context("failed to deploy and insert generic call forwarder v1")?; + + ierc20(token, provider.clone()) + .approve(PERMIT2_CANONICAL_ADDRESS, U256::MAX) + .send() + .await + .context("failed to submit WETH permit2 approval transaction")? + .get_receipt() + .await + .context("failed to fetch WETH permit2 approval receipt")?; + + Ok(()) +} diff --git a/crates/integration-test/tests/generic_call_action.rs b/crates/integration-test/tests/generic_call_action.rs new file mode 100644 index 0000000..e453ec6 --- /dev/null +++ b/crates/integration-test/tests/generic_call_action.rs @@ -0,0 +1,50 @@ +//! Chain-free smoke tests for the generic-call action fixtures: the builder +//! produces a well-formed action (one compliance unit) for valid and +//! deliberately-invalid override variants. No prover or chain is needed. + +use anoma_generic_call_forwarder_integration_test::fixtures::generic_call; +use anoma_generic_call_witness::GenericCall; + +fn sample_calls() -> Vec { + vec![GenericCall { + to: vec![0x11; 20], + value: 7, + data: vec![0xaa, 0xbb], + }] +} + +#[test] +fn build_produces_single_compliance_unit() { + let built = generic_call::build( + 1, + vec![0x22; 20], + sample_calls(), + generic_call::Overrides::default(), + ) + .expect("must build"); + assert_eq!(built.witnesses.compliance_witnesses.len(), 1); +} + +#[test] +fn build_with_invalid_consumed_non_ephemeral_still_builds() { + let built = generic_call::build( + 2, + vec![0x33; 20], + sample_calls(), + generic_call::Overrides::invalid_consumed_non_ephemeral(), + ) + .expect("must build invalid action"); + assert_eq!(built.witnesses.compliance_witnesses.len(), 1); +} + +#[test] +fn build_with_invalid_consumed_label_ref_still_builds() { + let built = generic_call::build( + 3, + vec![0x44; 20], + sample_calls(), + generic_call::Overrides::invalid_consumed_label_ref(), + ) + .expect("must build invalid action"); + assert_eq!(built.witnesses.compliance_witnesses.len(), 1); +} diff --git a/crates/integration-test/tests/swap_erc20s_on_a_dex.rs b/crates/integration-test/tests/swap_erc20s_on_a_dex.rs new file mode 100644 index 0000000..ca3c9b6 --- /dev/null +++ b/crates/integration-test/tests/swap_erc20s_on_a_dex.rs @@ -0,0 +1,311 @@ +//! Demonstrates a DEX swap routed through the generic-call forwarder: an ERC20 +//! resource of token A is unwrapped into the generic-call forwarder, swapped on +//! a (mock) Uniswap-style router for token B, and wrapped back into a resource. +//! +//! Mirrors the Solidity `test_calls_allow_swapping_fund_on_a_dex` in +//! `contracts/test/GenericCallForwarder.t.sol`, but as a full integration test: +//! every step is a risc0-proven transaction executed against a local chain. + +use alloy::primitives::aliases::{U48, U160}; +use alloy::primitives::{Address, U256}; +use alloy::sol; +use alloy::sol_types::SolCall; +use anoma_generic_call_forwarder_integration_test as it; +use anoma_pa_evm_integration_test::state::actors::default_signer; + +mod common; +use anoma_pa_evm_integration_test::keychain::EvmSigner; +use anoma_pa_evm_integration_test::state::chains::chain_id; +use anoma_pa_testkit::environment::CommitmentTree; +use anoma_pa_testkit::environment::Environment; +use anoma_pa_testkit::environment::ProtocolAdapter; +use anoma_pa_testkit::fixtures::identities; +use anomapay_erc20_forwarder_integration_test::deploy::erc20::example_erc20_bindings::{ + deploy_and_mint_example_erc20, erc20_example, +}; +use anomapay_erc20_forwarder_integration_test::deploy::permit2::PERMIT2_CANONICAL_ADDRESS; +use anomapay_erc20_forwarder_integration_test::fixtures::{transfer, unwrap, wrap}; +use anomapay_erc20_forwarder_integration_test::state::forwarder::addresses::erc20_forwarder_v1_address; +use anyhow::Context; +#[cfg(feature = "e2e")] +use common::setup_transfer_generic_call_e2e_env; +use common::{commitment_root, execute_tx, prove_actions, setup_transfer_generic_call_local_env}; +use it::deploy::dex_router::deploy_dex_router_mock; +use it::fixtures::generic_call; +use it::state::addresses::generic_call_forwarder_v1_address; +use rstest::*; + +use anoma_generic_call_witness::GenericCall; + +// Calldata interfaces for the calls the generic-call forwarder makes during the +// swap. Only the function selectors matter for encoding, so these are declared +// locally rather than pulled from full contract bindings. +sol! { + interface IErc20 { + function approve(address spender, uint256 amount) external returns (bool); + } + interface IPermit2 { + function approve(address token, address spender, uint160 amount, uint48 expiration) external; + } + interface IDexRouter { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] path, + address to, + uint256 deadline + ) external returns (uint256); + } +} + +async fn erc20_balance( + token: Address, + holder: Address, + provider: P, +) -> anyhow::Result { + erc20_example(token, provider) + .balanceOf(holder) + .call() + .await + .context("failed to read ERC20 balance") +} + +#[rstest] +#[case::local(setup_transfer_generic_call_local_env())] +#[cfg_attr(feature = "e2e", case::e2e_test(setup_transfer_generic_call_e2e_env()))] +#[tokio::test] +async fn calls_allow_to_swap_erc20s_on_a_dex( + #[future(awt)] + #[case] + env_with_setup: anyhow::Result, +) -> anyhow::Result<()> { + let mut env = env_with_setup.context("env setup failed")?; + let chain_id = chain_id(&env)?; + let erc20_forwarder = erc20_forwarder_v1_address(&env)?; + let generic_forwarder = generic_call_forwarder_v1_address(&env)?; + let provider = default_signer(&env).context("failed to retrieve default signer")?; + + let sender = identities::alice().context("failed to build sender keychain")?; + + let amount_in = 100u128; + let amount_out = 50u128; // the mock router pays out a fixed amountOutMin + let amount_in_u256 = U256::from(amount_in); + let amount_out_u256 = U256::from(amount_out); + + // Deploy token A (minted to the wrapper), the DEX router, and token B (minted + // to the router so it can pay out the swap). + let token_a = deploy_and_mint_example_erc20(provider.clone(), sender.address(), amount_in_u256) + .await + .context("failed to deploy token A")?; + let dex_router = deploy_dex_router_mock(provider.clone(), PERMIT2_CANONICAL_ADDRESS) + .await + .context("failed to deploy DEX router")?; + let token_b = deploy_and_mint_example_erc20(provider.clone(), dex_router, amount_out_u256) + .await + .context("failed to deploy token B")?; + + // The wrapper grants Permit2 an allowance so the wrap can pull token A. + erc20_example(token_a, provider.clone()) + .approve(PERMIT2_CANONICAL_ADDRESS, U256::MAX) + .send() + .await + .context("failed to submit token A Permit2 approval")? + .get_receipt() + .await + .context("failed to fetch token A Permit2 approval receipt")?; + + let before_root = commitment_root(&env)?; + + // 1. Wrap token A into a shielded resource. + let wrap = wrap::build( + chain_id, + erc20_forwarder, + token_a, + amount_in, + 11, + wrap::Overrides::default(), + ) + .await + .context("failed to build token A wrap action")?; + let tx = prove_actions(&env, &[wrap.witnesses]) + .await + .context("failed to prove token A wrap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute token A wrap action")?; + + // 2. Transfer the resource (sender -> receiver) so the receiver can unwrap it. + let transfer_path = env + .protocol_adapter() + .commitment_tree() + .path_to(wrap.created_persistent.commitment()) + .context("failed to generate transfer merkle path")?; + let transferred = transfer::build( + wrap.created_persistent, + erc20_forwarder, + token_a, + 17, + Some(transfer_path), + transfer::Overrides::default(), + ) + .context("failed to build transfer action")?; + let tx = prove_actions(&env, &[transferred.witnesses]) + .await + .context("failed to prove transfer action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute transfer action")?; + + // 3. Unwrap token A into the generic-call forwarder. + let unwrap_path = env + .protocol_adapter() + .commitment_tree() + .path_to(transferred.created_persistent.commitment()) + .context("failed to generate unwrap merkle path")?; + let unwrap = unwrap::build( + transferred.created_persistent, + erc20_forwarder, + token_a, + 21, + Some(unwrap_path), + unwrap::Overrides { + ethereum_account_addr: Some(generic_forwarder.to_vec()), + ..Default::default() + }, + ) + .context("failed to build unwrap action")?; + let tx = prove_actions(&env, &[unwrap.witnesses]) + .await + .context("failed to prove unwrap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute unwrap action")?; + + anyhow::ensure!( + erc20_balance(token_a, generic_forwarder, provider.clone()).await? == amount_in_u256, + "generic-call forwarder must hold token A after the unwrap" + ); + anyhow::ensure!( + erc20_balance(token_a, erc20_forwarder, provider.clone()).await? == U256::ZERO, + "ERC20 forwarder must hold no token A after the unwrap" + ); + + // 4. Generic call: from inside the forwarder, approve Permit2 + the router to + // pull token A, swap token A -> token B (paid back to the forwarder), and + // approve Permit2 to spend token B for the subsequent wrap. + let expiration = U48::from(4_294_967_295u64); + let calls = vec![ + GenericCall { + to: token_a.to_vec(), + value: 0, + data: IErc20::approveCall { + spender: PERMIT2_CANONICAL_ADDRESS, + amount: amount_in_u256, + } + .abi_encode(), + }, + GenericCall { + to: PERMIT2_CANONICAL_ADDRESS.to_vec(), + value: 0, + data: IPermit2::approveCall { + token: token_a, + spender: dex_router, + amount: U160::from(amount_in), + expiration, + } + .abi_encode(), + }, + GenericCall { + to: dex_router.to_vec(), + value: 0, + data: IDexRouter::swapExactTokensForTokensCall { + amountIn: amount_in_u256, + amountOutMin: amount_out_u256, + path: vec![token_a, token_b], + to: generic_forwarder, + deadline: U256::from(4_294_967_295u64), + } + .abi_encode(), + }, + GenericCall { + to: token_b.to_vec(), + value: 0, + data: IErc20::approveCall { + spender: PERMIT2_CANONICAL_ADDRESS, + amount: amount_out_u256, + } + .abi_encode(), + }, + ]; + + let swap = generic_call::build( + 31, + generic_forwarder.to_vec(), + calls, + generic_call::Overrides::default(), + ) + .context("failed to build generic call swap action")?; + + let tx = prove_actions(&env, &[swap.witnesses]) + .await + .context("failed to prove generic call swap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute generic call swap action")?; + + anyhow::ensure!( + erc20_balance(token_a, generic_forwarder, provider.clone()).await? == U256::ZERO, + "generic-call forwarder must hold no token A after the swap" + ); + anyhow::ensure!( + erc20_balance(token_b, generic_forwarder, provider.clone()).await? == amount_out_u256, + "generic-call forwarder must hold token B after the swap" + ); + anyhow::ensure!( + erc20_balance(token_a, dex_router, provider.clone()).await? == amount_in_u256, + "DEX router must have pulled token A during the swap" + ); + + // 5. Wrap token B (held by the generic-call forwarder) back into a resource. + // The forwarder authorizes the Permit2 pull via ERC-1271, so the permit + // signature is an accepted 65-byte placeholder. + let mut dummy_permit_sig = vec![0u8; 65]; + dummy_permit_sig[64] = 27; + let wrap_b = wrap::build( + chain_id, + erc20_forwarder, + token_b, + amount_out, + 41, + wrap::Overrides { + ethereum_account_addr: Some(generic_forwarder.to_vec()), + permit_signature: Some(dummy_permit_sig), + ..Default::default() + }, + ) + .await + .context("failed to build token B wrap action")?; + let tx = prove_actions(&env, &[wrap_b.witnesses]) + .await + .context("failed to prove token B wrap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute token B wrap action")?; + + anyhow::ensure!( + erc20_balance(token_b, erc20_forwarder, provider.clone()).await? == amount_out_u256, + "ERC20 forwarder must hold token B after the wrap-back" + ); + anyhow::ensure!( + erc20_balance(token_b, generic_forwarder, provider.clone()).await? == U256::ZERO, + "generic-call forwarder must hold no token B after the wrap-back" + ); + + let after_root = commitment_root(&env)?; + anyhow::ensure!( + before_root != after_root, + "commitment tree root must change" + ); + + Ok(()) +} diff --git a/crates/integration-test/tests/unwrap_native_tokens.rs b/crates/integration-test/tests/unwrap_native_tokens.rs new file mode 100644 index 0000000..373743a --- /dev/null +++ b/crates/integration-test/tests/unwrap_native_tokens.rs @@ -0,0 +1,249 @@ +use alloy::primitives::U256; +use alloy::providers::Provider; +use alloy::sol_types::SolCall; +use anoma_generic_call_forwarder_integration_test as it; +use anoma_pa_evm_integration_test::state::actors::default_signer; + +mod common; +use anoma_pa_evm_integration_test::keychain::EvmSigner; +use anoma_pa_evm_integration_test::state::chains::chain_id; +use anoma_pa_testkit::environment::CommitmentTree; +use anoma_pa_testkit::environment::Environment; +use anoma_pa_testkit::environment::ProtocolAdapter; +use anoma_pa_testkit::fixtures::identities; +use anomapay_erc20_forwarder_integration_test::deploy::erc20::ierc20_bindings::ierc20; +use anomapay_erc20_forwarder_integration_test::deploy::erc20::weth_bindings::WETH9; +use anomapay_erc20_forwarder_integration_test::fixtures::{transfer, unwrap, wrap}; +use anomapay_erc20_forwarder_integration_test::state::erc20::addresses::erc20_address; +use anomapay_erc20_forwarder_integration_test::state::forwarder::addresses::erc20_forwarder_v1_address; +use anyhow::Context; +#[cfg(feature = "e2e")] +use common::setup_transfer_generic_call_e2e_env; +use common::{commitment_root, execute_tx, prove_actions, setup_transfer_generic_call_local_env}; +use it::fixtures::generic_call; +use it::state::addresses::generic_call_forwarder_v1_address; +use rstest::*; + +use anoma_generic_call_witness::GenericCall; + +#[rstest] +#[case::local(setup_transfer_generic_call_local_env())] +#[cfg_attr(feature = "e2e", case::e2e_test(setup_transfer_generic_call_e2e_env()))] +#[tokio::test] +async fn calls_allow_to_unwrap_native_tokens( + #[future(awt)] + #[case] + env_with_setup: anyhow::Result, +) -> anyhow::Result<()> { + let mut env = env_with_setup.context("env setup failed")?; + let chain_id = chain_id(&env)?; + let erc20_forwarder = erc20_forwarder_v1_address(&env)?; + let generic_forwarder = generic_call_forwarder_v1_address(&env)?; + let weth = erc20_address(&env, "weth")?; + let provider = default_signer(&env).context("failed to retrieve default signer")?; + + let amount = 1u128; + let amount_u256 = U256::from(amount); + let sender = identities::alice().context("failed to build sender keychain")?; + let recipient = identities::bob().context("failed to build recipient keychain")?; + + let before_root = commitment_root(&env)?; + + let sender_weth_before = ierc20(weth, provider.clone()) + .balanceOf(sender.address()) + .call() + .await + .context("failed to read sender WETH balance before wrap")?; + let erc20_forwarder_weth_before_wrap = ierc20(weth, provider.clone()) + .balanceOf(erc20_forwarder) + .call() + .await + .context("failed to read ERC20 forwarder WETH balance before wrap")?; + let generic_forwarder_weth_before_unwrap = ierc20(weth, provider.clone()) + .balanceOf(generic_forwarder) + .call() + .await + .context("failed to read generic call forwarder WETH balance before unwrap")?; + let rand_seed = 11; + + let wrap = wrap::build( + chain_id, + erc20_forwarder, + weth, + amount, + rand_seed, + wrap::Overrides::default(), + ) + .await + .context("failed to build wrap action")?; + let tx = prove_actions(&env, &[wrap.witnesses]) + .await + .context("failed to prove wrap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute wrap action")?; + + let sender_weth_after_wrap = ierc20(weth, provider.clone()) + .balanceOf(sender.address()) + .call() + .await + .context("failed to read sender WETH balance after wrap")?; + anyhow::ensure!( + sender_weth_before - sender_weth_after_wrap == amount_u256, + "sender WETH must decrease by wrap amount" + ); + + let erc20_forwarder_weth_after_wrap = ierc20(weth, provider.clone()) + .balanceOf(erc20_forwarder) + .call() + .await + .context("failed to read ERC20 forwarder WETH balance after wrap")?; + anyhow::ensure!( + erc20_forwarder_weth_after_wrap - erc20_forwarder_weth_before_wrap == amount_u256, + "ERC20 forwarder WETH must equal wrap amount" + ); + + let created_persistent_merkle_path = env + .protocol_adapter() + .commitment_tree() + .path_to(wrap.created_persistent.commitment()) + .context("failed to generate transfer merkle path")?; + + let rand_seed = 17; + + let transfer = transfer::build( + wrap.created_persistent, + erc20_forwarder, + weth, + rand_seed, + Some(created_persistent_merkle_path), + transfer::Overrides::default(), + ) + .context("failed to build transfer action")?; + let tx = prove_actions(&env, &[transfer.witnesses]) + .await + .context("failed to prove transfer action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute transfer action")?; + + let unwrap_merkle_path = env + .protocol_adapter() + .commitment_tree() + .path_to(transfer.created_persistent.commitment()) + .context("failed to generate unwrap merkle path")?; + + let rand_seed = 21; + + let unwrap = unwrap::build( + transfer.created_persistent, + erc20_forwarder, + weth, + rand_seed, + Some(unwrap_merkle_path), + unwrap::Overrides { + ethereum_account_addr: Some(generic_forwarder.to_vec()), + ..unwrap::Overrides::default() + }, + ) + .context("failed to build unwrap action")?; + + let calls = vec![ + GenericCall { + to: weth.to_vec(), + value: 0, + data: WETH9::withdrawCall { wad: amount_u256 }.abi_encode(), + }, + GenericCall { + to: recipient.address().to_vec(), + value: amount, + data: Vec::new(), + }, + ]; + + let rand_seed = 31; + + let generic_call_action = generic_call::build( + rand_seed, + generic_forwarder.to_vec(), + calls, + generic_call::Overrides::default(), + ) + .context("failed to build generic call action")? + .witnesses; + + let tx = prove_actions(&env, &[unwrap.witnesses]) + .await + .context("failed to prove unwrap action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute unwrap action")?; + + let generic_forwarder_weth_after_unwrap = ierc20(weth, provider.clone()) + .balanceOf(generic_forwarder) + .call() + .await + .context("failed to read generic call forwarder WETH balance after unwrap")?; + anyhow::ensure!( + generic_forwarder_weth_after_unwrap - generic_forwarder_weth_before_unwrap == amount_u256, + "generic call forwarder WETH must equal unwrap amount before generic call" + ); + + let recipient_eth_before = provider + .get_balance(recipient.address()) + .await + .context("failed to read recipient ETH balance before")?; + + let tx = prove_actions(&env, &[generic_call_action]) + .await + .context("failed to prove generic call action")?; + execute_tx(&mut env, tx) + .await + .context("failed to execute generic call action")?; + + let erc20_forwarder_weth_after = ierc20(weth, provider.clone()) + .balanceOf(erc20_forwarder) + .call() + .await + .context("failed to read ERC20 forwarder WETH balance after generic call")?; + anyhow::ensure!( + erc20_forwarder_weth_after == U256::ZERO, + "ERC20 forwarder WETH should be zero after unwrap" + ); + + let generic_forwarder_weth_after = ierc20(weth, provider.clone()) + .balanceOf(generic_forwarder) + .call() + .await + .context("failed to read generic call forwarder WETH balance after generic call")?; + anyhow::ensure!( + generic_forwarder_weth_after == U256::ZERO, + "generic call forwarder WETH should be zero after withdraw" + ); + + let generic_forwarder_eth_after = provider + .get_balance(generic_forwarder) + .await + .context("failed to read generic call forwarder ETH balance after generic call")?; + anyhow::ensure!( + generic_forwarder_eth_after == U256::ZERO, + "generic call forwarder ETH should be zero after forwarding" + ); + + let recipient_eth_after = provider + .get_balance(recipient.address()) + .await + .context("failed to read recipient ETH balance after")?; + anyhow::ensure!( + recipient_eth_after - recipient_eth_before == amount_u256, + "recipient ETH must increase by transfer amount" + ); + + let after_root = commitment_root(&env)?; + anyhow::ensure!( + before_root != after_root, + "commitment tree root must change" + ); + + Ok(()) +} diff --git a/justfile b/justfile index 8b4c9cc..635f542 100644 --- a/justfile +++ b/justfile @@ -53,7 +53,7 @@ contracts-gen-bindings: cd contracts && forge clean && forge bind \ --skip test --skip script \ --select '^(GenericCallForwarder)$' \ - --bindings-path ../bindings/src/generated/ \ + --bindings-path ../crates/bindings/src/generated/ \ --module \ --overwrite @@ -103,28 +103,28 @@ contracts-publish version *args: # Clean bindings bindings-clean: - cd bindings && cargo clean + cd crates/bindings && cargo clean # Build bindings bindings-build *args: - cd bindings && cargo build {{ args }} + cd crates/bindings && cargo build {{ args }} # Test bindings bindings-test *args: - cd bindings && cargo test {{ args }} + cd crates/bindings && cargo test {{ args }} # Check bindings are up-to-date bindings-check: contracts-gen-bindings - git diff --exit-code bindings/src/generated/ + git diff --exit-code crates/bindings/src/generated/ # Publish bindings bindings-publish *args: - cd bindings && cargo publish {{ args }} + cd crates/bindings && cargo publish {{ args }} # Lint bindings (clippy) bindings-lint: - cd bindings && cargo clippy --no-deps -- -Dwarnings - cd bindings && cargo clippy --no-deps --tests -- -Dwarnings + cd crates/bindings && cargo clippy --no-deps -- -Dwarnings + cd crates/bindings && cargo clippy --no-deps --tests -- -Dwarnings # Format bindings bindings-fmt: @@ -134,42 +134,68 @@ bindings-fmt: bindings-fmt-check: cargo fmt -- --check +# --- Crates (workspace-wide Rust) --- + +# Clean all crates +crates-clean: + cargo clean + +# Build all crates +crates-build *args: + cargo build {{ args }} + +# Test all crates +crates-test *args: + cargo test {{ args }} + +# Lint all crates (clippy) +crates-lint: + cargo clippy --all-targets --no-deps -- -Dwarnings + +# Format all crates +crates-fmt *args: + cargo fmt --all {{ args }} + +# Check all crates formatting +crates-fmt-check: + cargo fmt --all -- --check + # --- All --- -# Lint all (contracts + bindings) +# Lint all (contracts + crates) all-lint: @echo "==> Linting contracts..." @just contracts-lint - @echo "==> Linting bindings..." - @just bindings-lint + @echo "==> Linting crates..." + @just crates-lint -# Format all (contracts + bindings) +# Format all (contracts + crates) all-fmt: @echo "==> Formatting contracts..." @just contracts-fmt - @echo "==> Formatting bindings..." - @just bindings-fmt + @echo "==> Formatting crates..." + @just crates-fmt -# Check formatting for all (contracts + bindings) +# Check formatting for all (contracts + crates) all-fmt-check: @echo "==> Checking contract formatting..." @just contracts-fmt-check - @echo "==> Checking bindings formatting..." - @just bindings-fmt-check + @echo "==> Checking crates formatting..." + @just crates-fmt-check -# Build all (contracts + bindings) +# Build all (contracts + crates) all-build: @echo "==> Building contracts..." @just contracts-build - @echo "==> Building bindings..." - @just bindings-build + @echo "==> Building crates..." + @just crates-build -# Test all (contracts + bindings) +# Test all (contracts + crates) all-test: @echo "==> Testing contracts..." @just contracts-test - @echo "==> Testing bindings..." - @just bindings-test + @echo "==> Testing crates..." + @just crates-test # Prerequisites check (mirrors CI) all-check: