diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index 919f41802f..67d111f5f1 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -64,6 +64,10 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@nightly + # `Build docs` below passes `-Zunstable-options`, so this job needs nightly rather than the + # toolchain `rust-toolchain.toml` pins. + - name: Use nightly for this job + run: echo "RUSTUP_TOOLCHAIN=nightly" >> "$GITHUB_ENV" - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd65e75198..180b87d1fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,11 @@ jobs: matrix: rust: ["1.88", "stable", "nightly"] flags: ["--no-default-features", "", "--all-features"] + # This matrix is the one place that deliberately compiles on something other than the pinned + # toolchain, so it has to outrank `rust-toolchain.toml`. `RUSTUP_TOOLCHAIN` does; the toolchain + # the install step selects does not. + env: + RUSTUP_TOOLCHAIN: ${{ matrix.rust }} steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master diff --git a/.gitignore b/.gitignore index 10e3bcae4d..855f2ab363 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ target .vscode .idea pkg/ +cache +out +broadcast +.env +all-chain.json bins/revme/temp_folder bins/revme/tests @@ -26,3 +31,5 @@ rustc-ice-* # Fixtures /test-fixtures + +node_modules \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e69de29bb2..ed45310f57 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "solidity/supra_contracts/lib/openzeppelin-contracts"] + path = solidity/supra_contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable"] + path = solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "solidity/supra_contracts/lib/forge-std"] + path = solidity/supra_contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/Cargo.lock b/Cargo.lock index dfba341dae..7b9b51ac45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,15 +13,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -35,6 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -55,22 +47,46 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alloy" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07655fedc35188f3c50ff8fc6ee45703ae14ef1bc7ae7d80e23a747012184e3" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-core", + "alloy-eips", + "alloy-genesis", + "alloy-network", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "alloy-trie", +] + [[package]] name = "alloy-chains" -version = "0.2.4" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9cc9d81ace3da457883b0bdf76776e55f1b84219a9e9d55c27ad308548d3f" +checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471" dependencies = [ "alloy-primitives", "num_enum", + "serde", "strum", ] [[package]] name = "alloy-consensus" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bcb57295c4b632b6b3941a089ee82d00ff31ff9eb3eac801bf605ffddc81041" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", @@ -79,6 +95,7 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -87,15 +104,16 @@ dependencies = [ "rand 0.8.5", "secp256k1 0.30.0", "serde", + "serde_json", "serde_with", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-consensus-any" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab669be40024565acb719daf1b2a050e6dc065fc0bec6050d97a81cdb860bd7" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -105,6 +123,57 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-contract" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d39c80ffc806f27a76ed42f3351a455f3dc4f81d6ff92c8aad2cf36b7d3a34" +dependencies = [ + "alloy-consensus", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-sol-types", + "alloy-transport", + "futures", + "futures-util", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-core" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a651e1d9e50e6d0a78bd23cd08facb70459a94501c4036c7799a093e569a310" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-rlp", + "alloy-sol-types", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d48a9101f4a67c22fae57489f1ddf3057b8ab4a368d8eac3be088b6e9d9c9d9" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "serde", + "serde_json", + "winnow", +] + [[package]] name = "alloy-eip2124" version = "0.2.0" @@ -115,38 +184,40 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "k256", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-eips" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f853de9ca1819f54de80de5d03bfc1bb7c9fafcf092b480a654447141bc354d" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -155,18 +226,36 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", "serde", + "serde_with", "sha2 0.10.9", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-genesis" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba4b1be0988c11f0095a2380aa596e35533276b8fa6c9e06961bbfe0aebcac5" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "borsh", + "serde", + "serde_with", ] [[package]] name = "alloy-json-abi" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26fdd571915bafe857fccba4ee1a4f352965800e46a53e4a5f50187b7776fa" +checksum = "9914c147bb9b25f440eca68a31dc29f5c22298bfa7754aa802965695384122b0" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -176,24 +265,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4997a9873c8639d079490f218e50e5fa07e70f957e9fc187c0a0535977f482f" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tracing", ] [[package]] name = "alloy-network" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0306e8d148b7b94d988615d367443c1b9d6d2e9fecd2e1f187ac5153dce56f5" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -212,14 +301,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-network-primitives" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eef189583f4c53d231dd1297b28a675ff842b551fb34715f562868a1937431a" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -230,21 +319,20 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a326d47106039f38b811057215a92139f46eef7983a4b77b10930a0ea5685b1e" +checksum = "7db950a29746be9e2f2c6288c8bd7a6202a81f999ce109a2933d2379970ec0fa" dependencies = [ "alloy-rlp", "arbitrary", "bytes", "cfg-if", "const-hex", - "derive_arbitrary", "derive_more", - "foldhash", + "foldhash 0.2.0", "getrandom 0.3.3", - "hashbrown 0.15.4", - "indexmap 2.9.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -252,6 +340,7 @@ dependencies = [ "proptest", "proptest-derive", "rand 0.9.1", + "rapidhash", "ruint", "rustc-hash", "serde", @@ -261,9 +350,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea624ddcdad357c33652b86aa7df9bd21afd2080973389d3facf1a221c573948" +checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -285,14 +374,13 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "http", "lru", "parking_lot", "pin-project", "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -323,15 +411,14 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43d00b4de38432304c4e4b01ae6a3601490fd9824c852329d158763ec18663c" +checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" dependencies = [ "alloy-json-rpc", "alloy-primitives", "alloy-transport", "alloy-transport-http", - "async-stream", "futures", "pin-project", "reqwest", @@ -341,16 +428,27 @@ dependencies = [ "tokio-stream", "tower", "tracing", - "tracing-futures", "url", "wasmtimer", ] +[[package]] +name = "alloy-rpc-types" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cf1398cb33aacb139a960fa3d8cf8b1202079f320e77e952a0b95967bf7a9f" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-any" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5958f2310d69f4806e6f6b90ceb4f2b781cc5a843517a7afe2e7cfec6de3cfb9" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -359,9 +457,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1826285e4ffc2372a8c061d5cc145858e67a0be3309b768c5b77ddb6b9e6cbc7" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -374,14 +472,15 @@ dependencies = [ "itertools 0.14.0", "serde", "serde_json", - "thiserror", + "serde_with", + "thiserror 2.0.17", ] [[package]] name = "alloy-serde" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "906ce0190afeded19cb2e963cb8507c975a7862216b9e74f39bf91ddee6ae74b" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "serde", @@ -390,9 +489,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89baab06195c4be9c5d66f15c55e948013d1aff3ec1cfb0ed469e1423313fce" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives", "async-trait", @@ -400,14 +499,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-signer-local" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a249a923e302ac6db932567c43945392f0b6832518aab3c4274858f58756774" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -416,14 +515,14 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "alloy-sol-macro" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4be1ce1274ddd7fdfac86e5ece1b225e9bba1f2327e20fbb30ee6b9cc1423fe" +checksum = "a3b96d5f5890605ba9907ce1e2158e2701587631dc005bfa582cf92dd6f21147" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -435,14 +534,15 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e92f3708ea4e0d9139001c86c051c538af0146944a2a9c7181753bd944bf57" +checksum = "b8247b7cca5cde556e93f8b3882b01dbd272f527836049083d240c57bf7b4c15" dependencies = [ + "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.9.0", + "indexmap 2.12.1", "proc-macro-error2", "proc-macro2", "quote", @@ -453,25 +553,27 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afe1bd348a41f8c9b4b54dfb314886786d6201235b0b3f47198b9d910c86bb2" +checksum = "3cd54f38512ac7bae10bbc38480eefb1b9b398ca2ce25db9cc0c048c6411c4f1" dependencies = [ + "alloy-json-abi", "const-hex", "dunce", "heck", "macro-string", "proc-macro2", "quote", + "serde_json", "syn 2.0.103", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6195df2acd42df92a380a8db6205a5c7b41282d0ce3f4c665ecf7911ac292f1" +checksum = "444b09815b44899564566d4d56613d14fa9a274b1043a021f00468568752f449" dependencies = [ "serde", "winnow", @@ -479,9 +581,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6185e98a79cf19010722f48a74b5a65d153631d2f038cabd250f4b9e9813b8ad" +checksum = "dc1038284171df8bfd48befc0c7b78f667a7e2be162f45f07bd1c378078ebe58" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -491,12 +593,12 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ae10b1bc77fde38161e242749e41e65e34000d05da0a3d3f631e03bfcb19e" +checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" dependencies = [ "alloy-json-rpc", - "alloy-primitives", + "auto_impl", "base64", "derive_more", "futures", @@ -504,7 +606,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tower", "tracing", @@ -514,9 +616,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b234272ee449e32c9f1afbbe4ee08ea7c4b52f14479518f95c844ab66163c545" +checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -529,9 +631,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" +checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -545,23 +647,16 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75ef8609ea2b31c799b0a56c724dca4c73105c5ccc205d9dfeb1d038df6a1da" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ - "alloy-primitives", - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.103", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -577,6 +672,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "annotate-snippets" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96401ca08501972288ecbcde33902fce858bf73fbcbdf91dab8c3a9544e106bb" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "0.6.19" @@ -594,9 +700,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -638,6 +744,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "ark-bls12-381" @@ -977,6 +1086,21 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "aurora-engine-modexp" version = "1.2.0" @@ -1010,21 +1134,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -1105,11 +1214,11 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1168,6 +1277,70 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.103", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.18.1" @@ -1180,6 +1353,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -1218,10 +1397,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -1231,17 +1411,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1375,7 +1560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1436,6 +1621,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1446,6 +1640,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1476,6 +1680,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion-plot" version = "0.5.0" @@ -1562,46 +1775,81 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", + "serde", "strsim", "syn 2.0.103", ] [[package]] -name = "darling_macro" -version = "0.20.11" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "darling_core", + "ident_case", + "proc-macro2", "quote", + "strsim", "syn 2.0.103", ] [[package]] -name = "dashmap" -version = "6.1.0" +name = "darling_macro" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "cfg-if", + "darling_core 0.21.3", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", "crossbeam-utils", "hashbrown 0.14.5", "lock_api", @@ -1621,12 +1869,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1640,6 +1888,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "derive-where" version = "1.5.0" @@ -1677,6 +1936,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ + "convert_case", "proc-macro2", "quote", "syn 2.0.103", @@ -1704,6 +1964,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1789,6 +2070,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-kinds" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -1815,6 +2116,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.12" @@ -1822,7 +2134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1834,7 +2146,7 @@ dependencies = [ "alloy-provider", "anyhow", "indicatif", - "revm", + "revm 29.0.0", "tokio", ] @@ -1843,7 +2155,7 @@ name = "example-cheatcode-inspector" version = "0.0.0" dependencies = [ "anyhow", - "revm", + "revm 29.0.0", ] [[package]] @@ -1851,14 +2163,14 @@ name = "example-contract-deployment" version = "0.0.0" dependencies = [ "anyhow", - "revm", + "revm 29.0.0", ] [[package]] name = "example-custom-opcodes" version = "0.0.0" dependencies = [ - "revm", + "revm 29.0.0", ] [[package]] @@ -1866,7 +2178,7 @@ name = "example-custom-precompile-journal" version = "0.1.0" dependencies = [ "anyhow", - "revm", + "revm 29.0.0", ] [[package]] @@ -1874,8 +2186,8 @@ name = "example-database-components" version = "0.0.0" dependencies = [ "auto_impl", - "revm", - "thiserror", + "revm 29.0.0", + "thiserror 2.0.17", ] [[package]] @@ -1885,7 +2197,7 @@ dependencies = [ "alloy-provider", "alloy-sol-types", "anyhow", - "revm", + "revm 29.0.0", "tokio", ] @@ -1893,7 +2205,7 @@ dependencies = [ name = "example-my-evm" version = "0.0.0" dependencies = [ - "revm", + "revm 29.0.0", ] [[package]] @@ -1904,7 +2216,7 @@ dependencies = [ "alloy-provider", "alloy-sol-types", "anyhow", - "revm", + "revm 29.0.0", "tokio", ] @@ -1916,10 +2228,20 @@ dependencies = [ "alloy-provider", "alloy-sol-types", "anyhow", - "revm", + "revm 29.0.0", "tokio", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1975,6 +2297,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml 0.8.23", + "uncased", + "version_check", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1987,6 +2329,17 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1999,6 +2352,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2023,6 +2382,158 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "foundry-block-explorers" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff814624bb21bfe43b70fb736ab39527b405d04cdc94d90b7e182fba28b25ec7" +dependencies = [ + "alloy-chains", + "alloy-json-abi", + "alloy-primitives", + "foundry-compilers", + "reqwest", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "foundry-compilers" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e639f98fe54d1cc0011a4bdb2eb1d838b379c9f004991ae7555a4cc09e8da32a" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "auto_impl", + "derive_more", + "dyn-clone", + "foundry-compilers-artifacts", + "foundry-compilers-core", + "itertools 0.14.0", + "path-slash", + "rayon", + "semver 1.0.26", + "serde", + "serde_json", + "sha2 0.10.9", + "solar-compiler", + "svm-rs", + "svm-rs-builds", + "thiserror 2.0.17", + "tracing", + "winnow", + "yansi", +] + +[[package]] +name = "foundry-compilers-artifacts" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ec96df20055211f4e46b5a61fa479b2ea7d1ce0659818e0359afadfcded8d2" +dependencies = [ + "foundry-compilers-artifacts-solc", + "foundry-compilers-artifacts-vyper", +] + +[[package]] +name = "foundry-compilers-artifacts-solc" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a206e475b5dd1a77dc33cd917cde4846148f5136729a24edb3a16ab431b90a" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "foundry-compilers-core", + "memchr", + "path-slash", + "rayon", + "regex", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", + "yansi", +] + +[[package]] +name = "foundry-compilers-artifacts-vyper" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74883db8036522fa21d0853c21ac318e165ec88e141f1ef1d6f7b4dfa841ff7" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "foundry-compilers-artifacts-solc", + "foundry-compilers-core", + "path-slash", + "semver 1.0.26", + "serde", +] + +[[package]] +name = "foundry-compilers-core" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab384daeaea5c33cad8c3c094a1eb6f98e70922e18380c660980c74c19e362b" +dependencies = [ + "alloy-primitives", + "cfg-if", + "dunce", + "path-slash", + "regex", + "semver 1.0.26", + "serde", + "serde_json", + "svm-rs", + "thiserror 2.0.17", + "tokio", + "walkdir", + "xxhash-rust", +] + +[[package]] +name = "foundry-config" +version = "1.4.1" +source = "git+https://github.com/foundry-rs/foundry.git?tag=v1.4.1#cf7746048646f2ecff48246dd61e265e49ab16f0" +dependencies = [ + "alloy-chains", + "alloy-primitives", + "clap", + "dirs", + "dunce", + "eyre", + "figment", + "foundry-block-explorers", + "foundry-compilers", + "glob", + "globset", + "heck", + "itertools 0.14.0", + "mesc", + "number_prefix", + "path-slash", + "rayon", + "regex", + "reqwest", + "revm 29.0.1", + "semver 1.0.26", + "serde", + "serde_json", + "solar-compiler", + "soldeer-core", + "thiserror 2.0.17", + "toml 0.9.11+spec-1.1.0", + "toml_edit 0.23.10+spec-1.0.0", + "tracing", + "walkdir", + "yansi", +] + [[package]] name = "funty" version = "2.0.0" @@ -2154,8 +2665,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2165,17 +2678,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.2" @@ -2183,13 +2692,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] -name = "gmp-mpfr-sys" -version = "1.6.5" +name = "globset" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d61197a68f6323b9afa616cf83d55d69191e1bf364d4eb7d35ae18defe776" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.8" +source = "git+ssh://git@github.com/Entropy-Foundation/gmp-mpfr-sys?branch=master#6d5c91968d809b692df0abdcc3d93ce3588ff7ad" dependencies = [ + "cc", "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2203,6 +2725,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -2239,8 +2780,18 @@ checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -2282,6 +2833,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.3.1" @@ -2331,6 +2891,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -2341,6 +2902,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -2375,10 +2954,12 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2519,12 +3100,28 @@ dependencies = [ ] [[package]] -name = "impl-codec" -version = "0.6.0" +name = "ignore" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ - "parity-scale-codec", + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", ] [[package]] @@ -2538,6 +3135,18 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "index_vec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" + [[package]] name = "indexmap" version = "1.9.3" @@ -2551,14 +3160,15 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -2574,6 +3184,25 @@ dependencies = [ "web-time", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "inturn" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2efbe120e37f17bb33fcdc82bc1c65087242608be37ace3cf7ebf49f3164e37" +dependencies = [ + "boxcar", + "bumpalo", + "dashmap", + "hashbrown 0.14.5", + "thread_local", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2598,7 +3227,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2651,10 +3280,12 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2717,9 +3348,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -2727,6 +3358,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.7.2" @@ -2797,9 +3438,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -2810,6 +3454,12 @@ dependencies = [ "hashbrown 0.15.4", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "macro-string" version = "0.1.4" @@ -2827,6 +3477,33 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mesc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04b0347d2799ef17df4623dbcb03531031142105168e0c549e0bf1f980e9e7e" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2834,6 +3511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2856,14 +3534,20 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + [[package]] name = "num" version = "0.4.3" @@ -2910,9 +3594,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -2994,31 +3678,23 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nybbles" -version = "0.3.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +checksum = "bfa11e84403164a9f12982ab728f3c67c6fd4ab5b5f0254ffc217bdbd3b28ab0" dependencies = [ "alloy-rlp", - "const-hex", + "cfg-if", "proptest", + "ruint", "serde", "smallvec", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3026,6 +3702,18 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "once_map" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29eefd5038c9eee9e788d90966d6b5578dd3f88363a91edaec117a7ae0adc2d5" +dependencies = [ + "ahash", + "hashbrown 0.16.1", + "parking_lot", + "stable_deref_trait", +] + [[package]] name = "oorandom" version = "11.1.5" @@ -3039,7 +3727,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "auto_impl", - "revm", + "revm 29.0.0", "rstest", "serde", "serde_json", @@ -3084,6 +3772,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.109" @@ -3096,6 +3790,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "p256" version = "0.13.2" @@ -3277,7 +3977,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3286,6 +3986,35 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.103", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3294,12 +4023,11 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", - "thiserror", "ucd-trie", ] @@ -3433,9 +4161,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -3461,6 +4189,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.103", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3487,7 +4225,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -3521,6 +4259,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", + "version_check", + "yansi", +] + [[package]] name = "proptest" version = "1.7.0" @@ -3543,9 +4294,9 @@ dependencies = [ [[package]] name = "proptest-derive" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" dependencies = [ "proc-macro2", "quote", @@ -3558,6 +4309,61 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -3650,11 +4456,21 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rapidhash" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e65c75143ce5d47c55b510297eeb1182f3c739b6043c537670e9fc18612dae" +dependencies = [ + "rand 0.9.1", + "rustversion", +] + [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -3662,9 +4478,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -3679,6 +4495,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -3701,9 +4528,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3713,9 +4540,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3742,18 +4569,28 @@ checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64", "bytes", + "encoding_rs", + "futures-channel", "futures-core", + "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -3761,34 +4598,57 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", ] [[package]] name = "revm" version = "29.0.0" dependencies = [ - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-handler", - "revm-inspector", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", + "revm-bytecode 6.2.2", + "revm-context 9.0.2", + "revm-context-interface 10.1.0", + "revm-database 7.0.5", + "revm-database-interface 7.0.5", + "revm-handler 10.0.0", + "revm-inspector 10.0.0", + "revm-interpreter 25.0.2", + "revm-precompile 27.0.0", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", "serde_json", ] +[[package]] +name = "revm" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718d90dce5f07e115d0e66450b1b8aa29694c1cf3f89ebddaddccc2ccbd2f13e" +dependencies = [ + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-context 9.1.0", + "revm-context-interface 10.2.0", + "revm-database 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-handler 10.0.1", + "revm-inspector 10.0.1", + "revm-interpreter 25.0.3", + "revm-precompile 27.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-bytecode" version = "6.2.2" @@ -3796,10 +4656,20 @@ dependencies = [ "bitvec", "paste", "phf", - "revm-primitives", + "revm-primitives 20.2.1", "serde", ] +[[package]] +name = "revm-bytecode" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c52031b73cae95d84cd1b07725808b5fd1500da3e5e24574a3b2dc13d9f16d" +dependencies = [ + "bitvec", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-context" version = "9.0.2" @@ -3807,15 +4677,31 @@ dependencies = [ "bitvec", "cfg-if", "derive-where", - "revm-bytecode", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-primitives", - "revm-state", + "revm-bytecode 6.2.2", + "revm-context-interface 10.1.0", + "revm-database 7.0.5", + "revm-database-interface 7.0.5", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", ] +[[package]] +name = "revm-context" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a20c98e7008591a6f012550c2a00aa36cba8c14cc88eb88dec32eb9102554b4" +dependencies = [ + "bitvec", + "cfg-if", + "derive-where", + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-context-interface 10.2.0", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-context-interface" version = "10.1.0" @@ -3824,12 +4710,27 @@ dependencies = [ "alloy-eip7702", "auto_impl", "either", - "revm-database-interface", - "revm-primitives", - "revm-state", + "revm-database-interface 7.0.5", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", ] +[[package]] +name = "revm-context-interface" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50d241ed1ce647b94caf174fcd0239b7651318b2c4c06b825b59b973dfb8495" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-database" version = "7.0.5" @@ -3837,27 +4738,51 @@ dependencies = [ "alloy-eips", "alloy-provider", "alloy-transport", - "revm-bytecode", - "revm-database-interface", - "revm-primitives", - "revm-state", + "revm-bytecode 6.2.2", + "revm-database-interface 7.0.5", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", "serde_json", "tokio", ] +[[package]] +name = "revm-database" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a276ed142b4718dcf64bc9624f474373ed82ef20611025045c3fb23edbef9c" +dependencies = [ + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-database-interface" version = "7.0.5" dependencies = [ "auto_impl", "either", - "revm-primitives", - "revm-state", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", "tokio", ] +[[package]] +name = "revm-database-interface" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c523c77e74eeedbac5d6f7c092e3851dbe9c7fec6f418b85992bd79229db361" +dependencies = [ + "auto_impl", + "either", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-ee-tests" version = "0.1.0" @@ -3865,7 +4790,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "op-revm", - "revm", + "revm 29.0.0", "rstest", "serde", "serde_json", @@ -3882,51 +4807,96 @@ dependencies = [ "alloy-signer-local", "auto_impl", "derive-where", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", + "revm-bytecode 6.2.2", + "revm-context 9.0.2", + "revm-context-interface 10.1.0", + "revm-database 7.0.5", + "revm-database-interface 7.0.5", + "revm-interpreter 25.0.2", + "revm-precompile 27.0.0", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", ] +[[package]] +name = "revm-handler" +version = "10.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550331ea85c1d257686e672081576172fe3d5a10526248b663bbf54f1bef226a" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-context 9.1.0", + "revm-context-interface 10.2.0", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-interpreter 25.0.3", + "revm-precompile 27.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-inspector" version = "10.0.0" dependencies = [ "auto_impl", "either", - "revm-context", - "revm-database", - "revm-database-interface", - "revm-handler", - "revm-interpreter", - "revm-primitives", - "revm-state", + "revm-context 9.0.2", + "revm-database 7.0.5", + "revm-database-interface 7.0.5", + "revm-handler 10.0.0", + "revm-interpreter 25.0.2", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "serde", "serde_json", ] +[[package]] +name = "revm-inspector" +version = "10.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0a6e9ccc2ae006f5bed8bd80cd6f8d3832cd55c5e861b9402fdd556098512f" +dependencies = [ + "auto_impl", + "either", + "revm-context 9.1.0", + "revm-database-interface 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-handler 10.0.1", + "revm-interpreter 25.0.3", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-state 7.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-interpreter" version = "25.0.2" dependencies = [ "bincode 2.0.1", - "revm-bytecode", - "revm-context-interface", - "revm-primitives", + "revm-bytecode 6.2.2", + "revm-context-interface 10.1.0", + "revm-primitives 20.2.1", "serde", ] [[package]] -name = "revm-precompile" -version = "27.0.0" +name = "revm-interpreter" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06575dc51b1d8f5091daa12a435733a90b4a132dca7ccee0666c7db3851bc30c" dependencies = [ - "ark-bls12-381", + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-context-interface 10.2.0", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "revm-precompile" +version = "27.0.0" +dependencies = [ + "ark-bls12-381", "ark-bn254", "ark-ec", "ark-ff 0.5.0", @@ -3943,7 +4913,7 @@ dependencies = [ "libsecp256k1", "p256", "rand 0.9.1", - "revm-primitives", + "revm-primitives 20.2.1", "ripemd", "rstest", "rug", @@ -3952,6 +4922,27 @@ dependencies = [ "substrate-bn", ] +[[package]] +name = "revm-precompile" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b57d4bd9e6b5fe469da5452a8a137bc2d030a3cd47c46908efc615bbc699da" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "cfg-if", + "k256", + "p256", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ripemd", + "sha2 0.10.9", +] + [[package]] name = "revm-primitives" version = "20.2.1" @@ -3962,25 +4953,74 @@ dependencies = [ "serde", ] +[[package]] +name = "revm-primitives" +version = "20.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa29d9da06fe03b249b6419b33968ecdf92ad6428e2f012dc57bcd619b5d94e" +dependencies = [ + "alloy-primitives", + "num_enum", + "once_cell", +] + [[package]] name = "revm-state" version = "7.0.5" dependencies = [ "bitflags", - "revm-bytecode", - "revm-primitives", + "revm-bytecode 6.2.2", + "revm-primitives 20.2.1", "serde", ] +[[package]] +name = "revm-state" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f64fbacb86008394aaebd3454f9643b7d5a782bd251135e17c5b33da592d84d" +dependencies = [ + "bitflags", + "revm-bytecode 6.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "revm-primitives 20.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "revm-statetest-types" version = "9.0.2" dependencies = [ "k256", - "revm", + "revm 29.0.0", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", +] + +[[package]] +name = "revm-supra-extension" +version = "0.1.0" +dependencies = [ + "alloy", + "alloy-consensus", + "alloy-contract", + "alloy-eips", + "alloy-serde", + "alloy-sol-types", + "anyhow", + "bincode 2.0.1", + "derive-getters", + "derive_more", + "enum-kinds", + "foundry-compilers", + "foundry-config", + "once_cell", + "revm-context 9.0.2", + "revm-primitives 20.2.1", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -3996,19 +5036,19 @@ dependencies = [ "indicatif", "k256", "plain_hasher", - "revm", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-inspector", - "revm-primitives", - "revm-state", + "revm 29.0.0", + "revm-bytecode 6.2.2", + "revm-context 9.0.2", + "revm-context-interface 10.1.0", + "revm-database 7.0.5", + "revm-database-interface 7.0.5", + "revm-inspector 10.0.0", + "revm-primitives 20.2.1", + "revm-state 7.0.5", "revm-statetest-types", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "triehash", "walkdir", ] @@ -4023,6 +5063,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -4118,12 +5172,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4167,7 +5215,33 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -4176,9 +5250,21 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -4212,6 +5298,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + [[package]] name = "schannel" version = "0.1.27" @@ -4233,6 +5328,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4302,7 +5415,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4310,9 +5436,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -4332,6 +5458,9 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -4344,10 +5473,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -4360,30 +5490,67 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn 2.0.103", ] +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.1", "itoa", "memchr", - "ryu", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4398,18 +5565,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.13.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", - "schemars", - "serde", - "serde_derive", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -4417,11 +5584,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.13.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.103", @@ -4497,6 +5664,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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" @@ -4507,6 +5684,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "siphasher" version = "1.0.1" @@ -4538,6 +5721,193 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "solar-ast" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6aaf98d032ba3be85dca5f969895ade113a9137bb5956f80c5faf14689de59" +dependencies = [ + "alloy-primitives", + "bumpalo", + "either", + "num-rational", + "semver 1.0.26", + "solar-data-structures", + "solar-interface", + "solar-macros", + "strum", +] + +[[package]] +name = "solar-compiler" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e792060bcbb007a6b9b060292945fb34ff854c7d93a9628f81b6c809eb4360" +dependencies = [ + "alloy-primitives", + "solar-ast", + "solar-config", + "solar-data-structures", + "solar-interface", + "solar-macros", + "solar-parse", + "solar-sema", +] + +[[package]] +name = "solar-config" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff16d692734c757edd339f5db142ba91b42772f8cbe1db1ce3c747f1e777185f" +dependencies = [ + "colorchoice", + "strum", +] + +[[package]] +name = "solar-data-structures" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dea34e58332c7d6a8cde1f1740186d31682b7be46e098b8cc16fcb7ffd98bf5" +dependencies = [ + "bumpalo", + "index_vec", + "indexmap 2.12.1", + "parking_lot", + "rayon", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "solar-interface" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6163af2e773f4d455212fa9ba2c0664506029dd26232eb406f5046092ac311" +dependencies = [ + "annotate-snippets", + "anstream", + "anstyle", + "derive_more", + "dunce", + "inturn", + "itertools 0.14.0", + "itoa", + "normalize-path", + "once_map", + "rayon", + "scoped-tls", + "serde", + "serde_json", + "solar-config", + "solar-data-structures", + "solar-macros", + "thiserror 2.0.17", + "tracing", + "unicode-width", +] + +[[package]] +name = "solar-macros" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a98045888d75d17f52e7b76f6098844b76078b5742a450c3ebcdbdb02da124" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "solar-parse" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b77a9cbb07948e4586cdcf64f0a483424197308816ebd57a4cf06130b68562" +dependencies = [ + "alloy-primitives", + "bitflags", + "bumpalo", + "itertools 0.14.0", + "memchr", + "num-bigint 0.4.6", + "num-rational", + "num-traits", + "ruint", + "smallvec", + "solar-ast", + "solar-data-structures", + "solar-interface", + "tracing", +] + +[[package]] +name = "solar-sema" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd033af43a38da316a04b25bbd20b121ce5d728b61e6988fd8fd6e2f1e68d0a1" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "bitflags", + "bumpalo", + "derive_more", + "either", + "once_map", + "paste", + "rayon", + "serde", + "serde_json", + "solar-ast", + "solar-data-structures", + "solar-interface", + "solar-macros", + "solar-parse", + "strum", + "thread_local", + "tracing", +] + +[[package]] +name = "soldeer-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6956940ce1436c00d50847e78c703375922d25130041a97f57508dee5f3952" +dependencies = [ + "bon", + "chrono", + "const-hex", + "derive_more", + "dunce", + "home", + "ignore", + "log", + "path-slash", + "rayon", + "regex", + "reqwest", + "sanitize-filename", + "semver 1.0.26", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.17", + "tokio", + "toml_edit 0.23.10+spec-1.0.0", + "uuid", + "zip", + "zip-extract", +] + [[package]] name = "sp1-lib" version = "5.0.5" @@ -4659,6 +6029,115 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "sval" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb9318255ebd817902d7e279d8f8e39b35b1b9954decd5eb9ea0e30e5fd2b6a" + +[[package]] +name = "sval_buffer" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12571299185e653fdb0fbfe36cd7f6529d39d4e747a60b15a3f34574b7b97c61" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39526f24e997706c0de7f03fb7371f7f5638b66a504ded508e20ad173d0a3677" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933dd3bb26965d682280fcc49400ac2a05036f4ee1e6dbd61bf8402d5a5c3a54" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0cda08f6d5c9948024a6551077557b1fdcc3880ff2f20ae839667d2ec2d87ed" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d49d5e6c1f9fd0e53515819b03a97ca4eb1bff5c8ee097c43391c09ecfb19f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f876c5a78405375b4e19cbb9554407513b59c93dea12dc6a4af4e1d30899ca" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9ccd3b7f7200239a655e517dd3fd48d960b9111ad24bd6a5e055bef17607c7" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svm-rs" +version = "0.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b159b54c22d9810087f0991371fd6242a912673e982a7c4ca8ea122f7e00a" +dependencies = [ + "const-hex", + "dirs", + "reqwest", + "semver 1.0.26", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", + "thiserror 2.0.17", + "url", + "zip", +] + +[[package]] +name = "svm-rs-builds" +version = "0.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab96ac3275ad299c6e5455b69a2f72443c4d3afb4933d92a0f859d48432dea49" +dependencies = [ + "const-hex", + "semver 1.0.26", + "serde_json", + "svm-rs", +] + [[package]] name = "syn" version = "1.0.109" @@ -4683,9 +6162,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c8c8f496c33dc6343dac05b4be8d9e0bca180a4caa81d7b8416b10cc2273cd" +checksum = "f6b1d2e2059056b66fec4a6bb2b79511d5e8d76196ef49c38996f4b48db7662f" dependencies = [ "paste", "proc-macro2", @@ -4713,6 +6192,27 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4729,27 +6229,56 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", ] [[package]] -name = "thiserror-impl" -version = "2.0.12" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.103", + "cfg-if", ] [[package]] @@ -4763,30 +6292,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4821,27 +6350,42 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.45.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", - "socket2", + "signal-hook-registry", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -4858,6 +6402,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -4883,11 +6437,50 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" @@ -4895,11 +6488,50 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", - "toml_datetime", + "indexmap 2.12.1", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.2" @@ -4977,18 +6609,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "futures", - "futures-task", - "pin-project", - "tracing", -] - [[package]] name = "tracing-subscriber" version = "0.2.25" @@ -5014,6 +6634,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -5044,17 +6670,38 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" @@ -5062,6 +6709,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unty" version = "0.0.4" @@ -5093,12 +6746,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -5108,6 +6762,42 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5171,48 +6861,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.103", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5220,26 +6894,39 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.103", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmtimer" version = "0.4.2" @@ -5256,9 +6943,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5274,13 +6961,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5291,16 +6987,16 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5309,9 +7005,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5324,13 +7020,30 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5339,7 +7052,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5348,7 +7061,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -5357,7 +7070,25 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -5366,14 +7097,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -5382,53 +7130,101 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5457,6 +7253,21 @@ dependencies = [ "tap", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] + [[package]] name = "yoke" version = "0.8.0" @@ -5574,3 +7385,52 @@ dependencies = [ "quote", "syn 2.0.103", ] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.12.1", + "memchr", + "zopfli", +] + +[[package]] +name = "zip-extract" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5b9958fd0b5b685af54f2c3fa21fca05fe295ebaf3e77b6d24d96c4174037" +dependencies = [ + "log", + "thiserror 2.0.17", + "zip", +] + +[[package]] +name = "zlib-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index dd4f8aaf67..e429f054f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ members = [ "crates/context", "crates/context/interface", "crates/handler", + # supra extensions + "crates/supra-extension", # variants "crates/op-revm", @@ -35,6 +37,7 @@ members = [ "examples/my_evm", "examples/custom_opcodes", "examples/custom_precompile_journal", + ] resolver = "2" default-members = ["crates/revm"] @@ -56,6 +59,7 @@ context-interface = { path = "crates/context/interface", package = "revm-context handler = { path = "crates/handler", package = "revm-handler", version = "10.0.0", default-features = false } op-revm = { path = "crates/op-revm", package = "op-revm", version = "10.0.0", default-features = false } ee-tests = { path = "crates/ee-tests", package = "revm-ee-tests", version = "0.1.0", default-features = false } +supra-extension = { path = "crates/supra-extension", package = "revm-supra-extension", version = "0.1.0", default-features = false } # alloy alloy-eip2930 = { version = "0.2.1", default-features = false } @@ -72,6 +76,19 @@ alloy-signer = { version = "1.0.12", default-features = false } alloy-signer-local = { version = "1.0.12", default-features = false } alloy-transport = { version = "1.0.12", default-features = false } +alloy-contract = { version = "1.0.19"} +alloy = { version = "1.0.19", features = ["sol-types", "contract"] } +alloy-serde = { version = "1.0.19" } + +# libraries required to build supra-contract bindings +# For more details see crates/supra-extension/build.rs +#forge = { git = "https://github.com/foundry-rs/foundry.git", tag="v1.4.1"} +#alloy-chains = { version = "0.2.13" } +#shlex = { version = "1.3.0" } +foundry-compilers = "0.19.14" +foundry-config = { git = "https://github.com/foundry-rs/foundry.git", tag="v1.4.1"} +toml = { version = "0.9.8"} + # precompiles ark-bls12-381 = { version = "0.5", default-features = false } ark-bn254 = { version = "0.5", default-features = false } @@ -103,7 +120,8 @@ criterion = { package = "codspeed-criterion-compat", version = "2.10" } # serde serde = { version = "1.0", default-features = false } -serde_json = { version = "1.0", default-features = false } +serde_json = { version = "1.0.149", default-features = false } +serde_with = "3.18.0" # misc auto_impl = "1.3.0" @@ -113,6 +131,9 @@ derive-where = { version = "1.5.0", default-features = false } rand = "0.9" tokio = "1.45" either = { version = "1.15.0", default-features = false } +derive_more = { version = "2.0.1" } +derive-getters = { version = "0.5.0" } +enum-kinds = { version = "0.5.1" } # dev-dependencies anyhow = "1.0.98" @@ -126,6 +147,8 @@ serde_derive = "1.0" thiserror = "2.0" triehash = "0.8" walkdir = "2.5" +gmp-mpfr-sys = "1.6.8" +once_cell = "1.21.4" [workspace.package] license = "MIT" @@ -146,6 +169,10 @@ rust.unreachable_pub = "warn" rust.unused_must_use = "deny" rustdoc.all = "warn" +[patch.crates-io] +gmp-mpfr-sys = { git = "ssh://git@github.com/Entropy-Foundation/gmp-mpfr-sys", branch="master" } + + [workspace.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/context/interface/src/cfg.rs b/crates/context/interface/src/cfg.rs index cd91706edc..9b2d29ba94 100644 --- a/crates/context/interface/src/cfg.rs +++ b/crates/context/interface/src/cfg.rs @@ -4,6 +4,67 @@ use core::fmt::Debug; use core::hash::Hash; use primitives::{hardfork::SpecId, Address, TxKind, U256}; +/// Describes execution context of the transaction. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub enum ExecutionMode { + #[default] + /// Executing user submitted transaction. + User, + /// Executing automated transaction. + Automated, + /// Executing governance sponsored automated transaction. + AutomatedGasless, + /// Executing a transaction for which no state change is expected + ReadOnly, + /// Executing governance native transaction. + System, + /// When transactions are executed in genesis mode. + Genesis, +} + +impl ExecutionMode { + /// Returns true if gas should be charged for execution. + pub fn charges_gas(&self) -> bool { + match self { + ExecutionMode::User | ExecutionMode::Automated => true, + ExecutionMode::AutomatedGasless + | ExecutionMode::System + | ExecutionMode::Genesis + | ExecutionMode::ReadOnly => false, + } + } + + /// Returns true if nonce should be updated in case of successful execution. + pub fn updates_nonce(&self) -> bool { + matches!(self, ExecutionMode::User | ExecutionMode::Genesis) + } + + /// Returns true if the execution context is for governance native transaction + pub fn is_system(&self) -> bool { + matches!(self, ExecutionMode::System) + } + + /// Returns true if the execution context is for governance genesis transaction + pub fn is_genesis(&self) -> bool { + matches!(self, ExecutionMode::Genesis) + } + + /// Returns true if the execution context is configured for read-only execution, + /// i.e. for execution of pure view functions. + pub fn is_read_only(&self) -> bool { + matches!(self, ExecutionMode::ReadOnly) + } + + /// Contract creation is supported only in user and genesis execution modes. + /// In Automation*, System mode contract deployment is not supported, as nonce update is not + /// expected in these 2 modes. + /// And ReadOnly mode is used to execute pure view transactions where *NO* state change is expected. + pub fn supports_contract_creation(&self) -> bool { + matches!(self, ExecutionMode::User | ExecutionMode::Genesis) + } +} + /// Configuration for the EVM. #[auto_impl(&, &mut, Box, Arc)] pub trait Cfg { @@ -58,6 +119,9 @@ pub trait Cfg { /// Returns whether the priority fee check is disabled. fn is_priority_fee_check_disabled(&self) -> bool; + + /// Returns whether the automation mode is enabled. + fn execution_mode(&self) -> &ExecutionMode; } /// What bytecode analysis to perform diff --git a/crates/context/interface/src/journaled_state.rs b/crates/context/interface/src/journaled_state.rs index df1ae23a8b..b839ddf300 100644 --- a/crates/context/interface/src/journaled_state.rs +++ b/crates/context/interface/src/journaled_state.rs @@ -97,7 +97,10 @@ pub trait JournalTr { balance: U256, ) -> Result, ::Error>; - /// Increments the balance of the account. + /// Marks the account as touched, creating journal entries + /// - balance updated with old-balance reference + /// - touched if the account was not yet touched globally in scope of execution session + /// - nonce update if input bump_nonce is true fn caller_accounting_journal_entry( &mut self, address: Address, @@ -215,6 +218,21 @@ pub trait JournalTr { /// any already committed changes and it is safe to call it multiple times. fn discard_tx(&mut self); + /// Returns `true` if the current transaction's journal contains any state-mutating entries. + /// + /// State-mutating entries are: storage writes (`SSTORE`), account creation (`CREATE`/`CREATE2`), + /// selfdestruct (`SELFDESTRUCT`), ETH transfers (`CALL` with non-zero value), nonce increments, + /// and code changes. `BalanceChange` entries are filtered by comparing the stored old balance + /// against the account's current balance in state, which eliminates false positives from the + /// unconditional `pre_execution` caller accounting entry emitted for zero-fee predicate calls. + /// + /// **Must be called before [`JournalTr::commit_tx`] or [`JournalTr::discard_tx`]** — both + /// of those methods clear the entry list, making detection impossible afterward. + /// + /// Used by [`Handler::execution_result`] to validate that [`ExecutionMode::ReadOnly`] + /// transactions are truly stateless. + fn has_state_mutations(&self) -> bool; + /// Clear current journal resetting it to initial state and return changes state. fn finalize(&mut self) -> Self::State; } diff --git a/crates/context/interface/src/lib.rs b/crates/context/interface/src/lib.rs index 0e66071368..499c0aba67 100644 --- a/crates/context/interface/src/lib.rs +++ b/crates/context/interface/src/lib.rs @@ -15,7 +15,7 @@ pub mod result; pub mod transaction; pub use block::Block; -pub use cfg::{Cfg, CreateScheme, TransactTo}; +pub use cfg::{Cfg, CreateScheme, ExecutionMode, TransactTo}; pub use context::{ContextError, ContextSetters, ContextTr}; pub use database_interface::{DBErrorMarker, Database}; pub use either; diff --git a/crates/context/interface/src/result.rs b/crates/context/interface/src/result.rs index 340da3a72b..1bc2b34568 100644 --- a/crates/context/interface/src/result.rs +++ b/crates/context/interface/src/result.rs @@ -434,6 +434,13 @@ pub enum InvalidTransaction { Eip7873NotSupported, /// EIP-7873 initcode transaction should have `to` address. Eip7873MissingTarget, + /// Unexpected transaction sender. + UnsupportedTransactionSender { + /// Sender address + sender: Address, + /// Reasoning of identified error + msg: String, + }, } impl TransactionError for InvalidTransaction {} @@ -528,6 +535,12 @@ impl fmt::Display for InvalidTransaction { Self::Eip7873MissingTarget => { write!(f, "Eip7873 initcode transaction should have `to` address") } + InvalidTransaction::UnsupportedTransactionSender { sender, msg } => { + write!( + f, + "Unsupported transaction sender. Sender: {sender}. Reason: {msg}" + ) + } } } } diff --git a/crates/context/interface/src/transaction.rs b/crates/context/interface/src/transaction.rs index 474d3e6ff7..ac7ed0df68 100644 --- a/crates/context/interface/src/transaction.rs +++ b/crates/context/interface/src/transaction.rs @@ -70,6 +70,11 @@ pub trait Transaction { /// Note : Common field for all transactions. fn nonce(&self) -> u64; + /// Transaction hash (32 bytes). + /// + /// Note : Common field for all transactions. + fn tx_hash(&self) -> B256; + /// Transaction kind. It can be Call or Create. /// /// Kind is applicable for: Legacy, EIP-2930, EIP-1559 diff --git a/crates/context/interface/src/transaction/either.rs b/crates/context/interface/src/transaction/either.rs index c4d6413de7..857ea3f151 100644 --- a/crates/context/interface/src/transaction/either.rs +++ b/crates/context/interface/src/transaction/either.rs @@ -118,4 +118,11 @@ where Either::Right(r) => r.max_priority_fee_per_gas(), } } + + fn tx_hash(&self) -> B256 { + match self { + Either::Left(l) => l.tx_hash(), + Either::Right(r) => r.tx_hash(), + } + } } diff --git a/crates/context/src/cfg.rs b/crates/context/src/cfg.rs index 788b358cfc..fc457c1456 100644 --- a/crates/context/src/cfg.rs +++ b/crates/context/src/cfg.rs @@ -1,7 +1,8 @@ //! This module contains [`CfgEnv`] and implements [`Cfg`] trait for it. +use context_interface::cfg::ExecutionMode; pub use context_interface::Cfg; - use primitives::{eip170, eip3860, eip7825, hardfork::SpecId}; + /// EVM configuration #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Eq, PartialEq)] @@ -104,6 +105,8 @@ pub struct CfgEnv { /// By default, it is set to `false`. #[cfg(feature = "optional_priority_fee_check")] pub disable_priority_fee_check: bool, + /// Execution mode the EVM is configured to run. + pub execution_mode: ExecutionMode, } impl CfgEnv { @@ -159,6 +162,7 @@ impl CfgEnv { disable_base_fee: false, #[cfg(feature = "optional_priority_fee_check")] disable_priority_fee_check: false, + execution_mode: ExecutionMode::User, } } @@ -206,6 +210,7 @@ impl CfgEnv { disable_base_fee: self.disable_base_fee, #[cfg(feature = "optional_priority_fee_check")] disable_priority_fee_check: self.disable_priority_fee_check, + execution_mode: self.execution_mode, } } @@ -344,6 +349,10 @@ impl + Copy> Cfg for CfgEnv { } } } + + fn execution_mode(&self) -> &ExecutionMode { + &self.execution_mode + } } impl Default for CfgEnv { diff --git a/crates/context/src/journal.rs b/crates/context/src/journal.rs index 206abbb4e8..f3465391c0 100644 --- a/crates/context/src/journal.rs +++ b/crates/context/src/journal.rs @@ -291,6 +291,14 @@ impl JournalTr for Journal { self.inner.discard_tx(); } + #[inline] + fn has_state_mutations(&self) -> bool { + self.inner + .journal + .iter() + .any(|entry| entry.is_state_mutating(&self.inner.state)) + } + /// Clear current journal resetting it to initial state and return changes state. #[inline] fn finalize(&mut self) -> Self::State { diff --git a/crates/context/src/journal/entry.rs b/crates/context/src/journal/entry.rs index 00b5f17219..e074d5920f 100644 --- a/crates/context/src/journal/entry.rs +++ b/crates/context/src/journal/entry.rs @@ -5,7 +5,8 @@ //! They are created when there is change to the state from loading (making it warm), changes to the balance, //! or removal of the storage slot. Check [`JournalEntryTr`] for more details. -use primitives::{Address, StorageKey, StorageValue, KECCAK_EMPTY, PRECOMPILE3, U256}; +use bytecode::Bytecode; +use primitives::{Address, StorageKey, StorageValue, B256, PRECOMPILE3, U256}; use state::{EvmState, TransientStorage}; /// Trait for tracking and reverting state changes in the EVM. @@ -57,8 +58,45 @@ pub trait JournalEntryTr { had_value: StorageValue, ) -> Self; - /// Creates a journal entry for when an account's code is modified - fn code_changed(address: Address) -> Self; + /// Creates a journal entry for when an account's code is modified. + /// Records the previous code/hash so revert can restore it - the + /// previous value is not necessarily empty (e.g. EIP-7702 explicitly + /// permits re-delegating an authority that already holds a delegation). + fn code_changed( + address: Address, + previous_code_hash: B256, + previous_code: Option, + ) -> Self; + + /// Returns `true` if this journal entry represents an operation that mutates persistent state. + /// + /// Used to verify that [`ExecutionMode::ReadOnly`] transactions are truly + /// stateless before committing or discarding them. + /// + /// `BalanceChange` requires the current `state` snapshot to filter out zero-delta entries + /// produced unconditionally by `pre_execution` for zero-fee predicate calls — the old + /// balance stored in the entry is compared against the account's current balance. + /// + /// Read-only / gas-metering entries (`AccountWarmed`, `AccountTouched`, `StorageWarmed`, + /// `TransientStorageChange`) always return `false`. + fn is_state_mutating(&self, state: &EvmState) -> bool; + + /// Returns the amount of native value this entry destroyed outright, with no + /// credit to any other account. + /// + /// Only a self-destruct whose beneficiary is the self-destructing account itself + /// destroys value: the balance is zeroed and nothing is credited anywhere. A + /// self-destruct to any other address moves the balance, and every other journal + /// entry conserves it, so all of those return zero. + /// + /// The value is reported from the entry rather than from a state diff so that it + /// disappears together with the entry when a frame or a transaction is reverted. + /// + /// Defaults to zero so that an entry type which has no notion of a destroyed + /// balance does not have to implement it. + fn selfdestruct_burn(&self) -> U256 { + U256::ZERO + } /// Reverts the state change recorded by this journal entry /// @@ -212,9 +250,65 @@ pub enum JournalEntry { CodeChange { /// Address of account that had its code changed. address: Address, + /// Code hash of the account before this change. + /// + /// Not necessarily `KECCAK_EMPTY` - e.g. EIP-7702 explicitly permits + /// re-delegating an authority that already holds a delegation, so + /// the previous code can be a real, non-empty value that must be + /// restored on revert, not wiped to empty. + previous_code_hash: B256, + /// Code of the account before this change. + previous_code: Option, }, } impl JournalEntryTr for JournalEntry { + fn selfdestruct_burn(&self) -> U256 { + match self { + // `JournalInner::selfdestruct` only credits the beneficiary when it is a + // different account, so a self-targeting destruction zeroes the balance + // without crediting anyone: that balance is destroyed. + JournalEntry::AccountDestroyed { + address, + target, + had_balance, + .. + } if address == target => *had_balance, + _ => U256::ZERO, + } + } + + fn is_state_mutating(&self, state: &EvmState) -> bool { + match self { + // SSTORE wrote a storage value. + JournalEntry::StorageChanged { .. } => true, + // CREATE / CREATE2 deployed a new contract. + JournalEntry::AccountCreated { .. } => true, + // SELFDESTRUCT destroyed a contract. + JournalEntry::AccountDestroyed { .. } => true, + // Nonce was incremented (must not occur in ReadOnly mode). + JournalEntry::NonceChange { .. } => true, + // Contract bytecode was replaced. + JournalEntry::CodeChange { .. } => true, + // ETH transferred via a CALL with non-zero value. + JournalEntry::BalanceTransfer { balance, .. } => !balance.is_zero(), + // Balance was changed. pre_execution unconditionally pushes a BalanceChange + // entry even for zero-fee predicate calls, so filter those out by comparing + // the stored old_balance against the account's current balance in state. + JournalEntry::BalanceChange { + address, + old_balance, + } => state + .get(address) + .map(|account| account.info.balance != *old_balance) + .unwrap_or(true), + // Read-only / gas-metering entries — never mutations. + JournalEntry::AccountWarmed { .. } + | JournalEntry::AccountTouched { .. } + | JournalEntry::StorageWarmed { .. } + | JournalEntry::TransientStorageChange { .. } => false, + } + } + fn account_warmed(address: Address) -> Self { JournalEntry::AccountWarmed { address } } @@ -283,8 +377,16 @@ impl JournalEntryTr for JournalEntry { } } - fn code_changed(address: Address) -> Self { - JournalEntry::CodeChange { address } + fn code_changed( + address: Address, + previous_code_hash: B256, + previous_code: Option, + ) -> Self { + JournalEntry::CodeChange { + address, + previous_code_hash, + previous_code, + } } fn revert( @@ -400,10 +502,14 @@ impl JournalEntryTr for JournalEntry { transient_storage.insert(tkey, had_value); } } - JournalEntry::CodeChange { address } => { + JournalEntry::CodeChange { + address, + previous_code_hash, + previous_code, + } => { let acc = state.get_mut(&address).unwrap(); - acc.info.code_hash = KECCAK_EMPTY; - acc.info.code = None; + acc.info.code_hash = previous_code_hash; + acc.info.code = previous_code; } } } diff --git a/crates/context/src/journal/inner.rs b/crates/context/src/journal/inner.rs index ad33850471..f5dd31208a 100644 --- a/crates/context/src/journal/inner.rs +++ b/crates/context/src/journal/inner.rs @@ -55,6 +55,42 @@ pub struct JournalInner { pub spec: SpecId, /// Warm addresses containing both coinbase and current precompiles. pub warm_addresses: WarmAddresses, + /// Running total of native value destroyed by `SELFDESTRUCT` with the destroyed + /// account as its own beneficiary, which credits no account and so removes the + /// value from circulation. + /// + /// Supra's EVM native balance mirrors value escrowed outside the EVM and cannot be + /// minted, so the host has to account for every destruction of it. The total is + /// accumulated where the destruction decision is made and is decremented again when + /// the journal entry that recorded it is reverted, so a reverted frame or a + /// discarded transaction contributes nothing. + /// + /// Unlike the other fields this one is deliberately **not** cleared by + /// [`Self::commit_tx`] or [`Self::finalize`], so that it is still readable after + /// execution has finished. It is a running total since the last + /// [`Self::take_selfdestruct_burn`]. + /// + /// # Drain between transactions only + /// + /// [`Self::take_selfdestruct_burn`] must be called between transactions, never + /// part-way through one. A later revert of an already-drained destruction has + /// nothing left to take back, and the drained total keeps a burn that did not + /// happen. That makes this discipline a requirement on the caller rather than + /// advice. + /// + /// A violation is caught by a `debug_assert!` at each revert site, so it is loud in + /// tests and debug builds. Release builds subtract with `saturating_sub` instead: + /// on a consensus-critical path a broken caller should cost accounting accuracy, + /// not a halt. + /// + /// # Scope is whatever the caller drains + /// + /// The journal has no notion of a block. Supra runs one journal per block and + /// drains once per transaction, which is what makes the sum of the drains a block + /// total. An embedder that finalizes per transaction instead gets a + /// per-transaction total from the same field, and those still compose, because + /// each drain covers exactly the destructions since the previous one. + pub selfdestruct_burn: U256, } impl Default for JournalInner { @@ -78,9 +114,22 @@ impl JournalInner { depth: 0, spec: SpecId::default(), warm_addresses: WarmAddresses::new(), + selfdestruct_burn: U256::ZERO, } } + /// Returns the accumulated self-destruct burn total and resets it to zero. + /// + /// Call this only between transactions. Draining part-way through one leaves the + /// total wrong if the destruction is later reverted; that is caught by a + /// `debug_assert!` in debug builds and tolerated in release. See + /// [`Self::selfdestruct_burn`] for that contract, and for what is counted and over + /// what scope. + #[inline] + pub fn take_selfdestruct_burn(&mut self) -> U256 { + mem::take(&mut self.selfdestruct_burn) + } + /// Returns the logs #[inline] pub fn take_logs(&mut self) -> Vec { @@ -106,10 +155,14 @@ impl JournalInner { transaction_id, spec, warm_addresses, + selfdestruct_burn, } = self; // Spec precompiles and state are not changed. It is always set again execution. let _ = spec; let _ = state; + // Preserved across the transaction boundary so the host can still read it after + // execution; see the field docs. + let _ = selfdestruct_burn; transient_storage.clear(); *depth = 0; @@ -135,12 +188,23 @@ impl JournalInner { transaction_id, spec, warm_addresses, + selfdestruct_burn, } = self; let is_spurious_dragon_enabled = spec.is_enabled_in(SPURIOUS_DRAGON); // iterate over all journals entries and revert our global state + let mut reverted_burn = U256::ZERO; journal.drain(..).rev().for_each(|entry| { + reverted_burn += entry.selfdestruct_burn(); entry.revert(state, None, is_spurious_dragon_enabled); }); + // A discarded transaction destroyed nothing, so take back everything it added. + debug_assert!( + *selfdestruct_burn >= reverted_burn, + "reverting a self-destruct burn the accumulator no longer holds: \ + the accumulator was drained mid-transaction; drain only between transactions" + ); + // Saturating for the same reason as in `checkpoint_revert`. + *selfdestruct_burn = selfdestruct_burn.saturating_sub(reverted_burn); transient_storage.clear(); *depth = 0; logs.clear(); @@ -167,9 +231,13 @@ impl JournalInner { transaction_id, spec, warm_addresses, + selfdestruct_burn, } = self; // Spec is not changed. And it is always set again in execution. let _ = spec; + // Preserved across the transaction boundary so the host can still read it after + // execution; see the field docs. + let _ = selfdestruct_burn; // Clear coinbase address warming for next tx warm_addresses.clear_coinbase(); @@ -239,7 +307,16 @@ impl JournalInner { let account = self.state.get_mut(&address).unwrap(); Self::touch_account(&mut self.journal, address, account); - self.journal.push(ENTRY::code_changed(address)); + // Capture the previous code/hash so a revert restores it exactly - + // it is not necessarily empty (e.g. EIP-7702 explicitly permits + // re-delegating an authority that already holds a delegation). + let previous_code_hash = account.info.code_hash; + let previous_code = account.info.code.clone(); + self.journal.push(ENTRY::code_changed( + address, + previous_code_hash, + previous_code, + )); account.info.code_hash = hash; account.info.code = Some(code); @@ -274,8 +351,16 @@ impl JournalInner { // account balance changed. self.journal .push(ENTRY::balance_changed(address, old_balance)); - // account is touched. - self.journal.push(ENTRY::account_touched(address)); + // Mark the caller touched through the guarded `touch()` helper rather than pushing an + // unconditional `AccountTouched` entry. `Touched` is a sticky, block-scoped flag (see + // `AccountStatus` docs) — once a prior transaction has committed with this account + // touched, a later transaction must NOT record another touch entry for it, otherwise + // discarding that later transaction (e.g. a `ExecutionMode::ReadOnly` predicate call) + // would incorrectly unmark the account as touched, dropping the earlier committed + // transaction's changes from the state diff at `finalize()`. The caller must not call + // `mark_touch()` itself before this, or the guard below would always see the account as + // already touched and never record the entry needed to undo a genuine first touch. + self.touch(address); if bump_nonce { // nonce changed. @@ -467,12 +552,23 @@ impl JournalInner { // iterate over last N journals sets and revert our global state if checkpoint.journal_i < self.journal.len() { + let mut reverted_burn = U256::ZERO; self.journal .drain(checkpoint.journal_i..) .rev() .for_each(|entry| { + reverted_burn += entry.selfdestruct_burn(); entry.revert(state, Some(transient_storage), is_spurious_dragon_enabled); }); + // The destruction did not happen, so it must not be reported. + debug_assert!( + self.selfdestruct_burn >= reverted_burn, + "reverting a self-destruct burn the accumulator no longer holds: \ + the accumulator was drained mid-transaction; drain only between transactions" + ); + // Saturating in release so that a caller that breaks the drain contract + // costs accounting accuracy rather than halting the node. + self.selfdestruct_burn = self.selfdestruct_burn.saturating_sub(reverted_burn); } } @@ -522,10 +618,18 @@ impl JournalInner { let is_cancun_enabled = spec.is_enabled_in(CANCUN); + // Value destroyed outright by this call, credited to no account. + let mut burned = U256::ZERO; + // EIP-6780 (Cancun hard-fork): selfdestruct only if contract is created in the same tx let journal_entry = if acc.is_created_locally() || !is_cancun_enabled { acc.mark_selfdestructed_locally(); acc.info.balance = U256::ZERO; + // The transfer above runs only for a distinct beneficiary, so when the + // account is its own beneficiary this zeroing destroys the balance. + if address == target { + burned = balance; + } Some(ENTRY::account_destroyed( address, target, @@ -547,6 +651,8 @@ impl JournalInner { self.journal.push(entry); }; + self.selfdestruct_burn += burned; + Ok(StateLoad { data: SelfDestructResult { had_value: !balance.is_zero(), @@ -631,29 +737,87 @@ impl JournalInner { let load = match self.state.entry(address) { Entry::Occupied(entry) => { let account = entry.into_mut(); - let is_cold = account.mark_warm_with_transaction_id(self.transaction_id); - // if it is colad loaded we need to clear local flags that can interact with selfdestruct - if is_cold { + // `is_new_tx_touch` reflects only whether *this specific + // account* has been touched by the current transaction yet - + // it drives the per-tx lazy invalidation below regardless of + // gas-warmth semantics. + let is_new_tx_touch = account.mark_warm_with_transaction_id(self.transaction_id); + // if it is cold loaded we need to clear local flags that can interact with selfdestruct + if is_new_tx_touch { // if it is cold loaded and we have selfdestructed locally it means that // account was selfdestructed in previous transaction and we need to clear its information and storage. if account.is_selfdestructed_locally() { account.selfdestruct(); account.unmark_selfdestructed_locally(); + // Only clear the persistent global flags if this + // account was itself created somewhere in this + // journal's own history (`is_created()`, otherwise + // only cleared by an `AccountCreated` revert, which + // un-creates the account for the same reason) - that's + // the actual invariant that makes "blank slate" safe, + // not the hardfork: + // an account created within this journal provably + // has no pre-existing on-disk storage predating this + // block, so treating it as fully fresh cannot lose + // anything. This is always true post-Cancun when + // this branch is reached at all (`is_selfdestructed_locally()` + // there requires `is_created_locally()`, which always + // sets the global `Created` flag too), and lets a + // later recreate of that same account correctly show + // as alive rather than still destroyed (EIP-6780), + // matching what a standalone, finalize-per-tx + // execution of the same sequence would show. + // + // Pre-Cancun, `selfdestruct()` also sets this flag + // for a cross-tx destroy of a pre-existing contract + // that was never created in this journal at all + // (`is_created()` false) - there, destruction is + // permanent per legacy semantics, and clearing the + // global flags would incorrectly let + // `apply_account_state`/`CacheDB::commit` skip wiping + // that contract's real, possibly large pre-existing + // on-disk storage (which this journal never loaded + // and has no way to enumerate), resurrecting old data + // if the address is ever touched again. + if account.is_created() { + account.unmark_selfdestruct(); + account.unmark_created(); + } } // unmark locally created account.unmark_created_locally(); } + // Coinbase and precompile addresses are re-warmed at the + // start of every transaction (EIP-3651 / EIP-2929), + // independent of whether an earlier transaction sharing this + // journal already loaded them into `state`. The + // transaction_id check above only knows "has *this* + // transaction touched this specific account before" - it has + // no notion of the perpetually pre-warmed set, so an address + // that's supposed to always start warm would otherwise be + // incorrectly charged a cold access on every transaction + // after the first one to ever load it in this block. + let is_cold = is_new_tx_touch && self.warm_addresses.is_cold(&address); StateLoad { data: account, is_cold, } } Entry::Vacant(vac) => { - let account = if let Some(account) = db.basic(address)? { + let mut account = if let Some(account) = db.basic(address)? { account.into() } else { Account::new_not_existing(self.transaction_id) }; + // `From for Account` doesn't know about the + // journal's current transaction and hardcodes + // transaction_id to 0 - stamp the real current id here so + // this account's warmth is tracked correctly on its next + // touch within THIS transaction (mark_warm_with_transaction_id + // compares against this value), regardless of how many + // earlier, unrelated transactions have already run in this + // journal and advanced transaction_id past 0. + account.transaction_id = self.transaction_id; // Precompiles among some other account(coinbase included) are warm loaded so we need to take that into account let is_cold = self.warm_addresses.is_cold(&address); @@ -833,7 +997,24 @@ pub fn sload_with_account( let (value, is_cold) = match account.storage.entry(key) { Entry::Occupied(occ) => { let slot = occ.into_mut(); + // EIP-2200/EIP-3529: "original value" must track the value at the + // start of the CURRENT transaction ("what the value would be if + // the current transaction is reverted"). If this slot was last + // touched by an *earlier*, already-committed transaction sharing + // this journal (revm's single-journal, multi-tx execution mode, + // e.g. `transact_many()`), its transaction_id will differ from + // the current one - that earlier transaction's final + // `present_value` is this transaction's correct origin baseline. + // + // This check is keyed off `transaction_id`, not the `is_cold` + // return value below, because `is_cold` can also become true + // from an in-transaction revert re-marking the slot cold (same + // transaction_id), which must NOT reset `original_value`. + let is_new_tx = slot.transaction_id != transaction_id; let is_cold = slot.mark_warm_with_transaction_id(transaction_id); + if is_new_tx { + slot.original_value = slot.present_value; + } (slot.present_value, is_cold) } Entry::Vacant(vac) => { @@ -857,3 +1038,969 @@ pub fn sload_with_account( Ok(StateLoad::new(value, is_cold)) } + +#[cfg(test)] +mod eip2200_original_value_tests { + use super::*; + use crate::JournalEntry; + use database::{CacheDB, EmptyDB}; + + /// EIP-2200 defines "original value" as "what the value would be if the + /// CURRENT transaction is reverted" - i.e. the slot's value at the start + /// of the transaction currently executing. + /// + /// This test checks that `EvmStorageSlot::original_value` is refreshed + /// at the transaction boundary (`commit_tx`) when a single `JournalInner` + /// is reused across multiple transactions in the same block (revm's + /// single-journal, multi-tx execution model, e.g. `transact_many()`). + #[test] + fn original_value_should_track_start_of_current_tx_not_start_of_block() { + let mut journal = JournalInner::::new(); + let mut db = EmptyDB::new(); + + let addr = Address::with_last_byte(1); + let key = StorageKey::from(1); + let value_a = StorageValue::from(42); + + // --- Transaction 1 (of this block): slot goes 0 -> A, then commits --- + journal.load_account(&mut db, addr).unwrap(); + let t1 = journal.sstore(&mut db, addr, key, value_a).unwrap().data; + assert_eq!( + t1.original_value, + StorageValue::ZERO, + "T1 original should be pre-block DB value (0)" + ); + assert_eq!(t1.present_value, StorageValue::ZERO); + assert_eq!(t1.new_value, value_a); + + // Transaction boundary: T1 is done and committed, T2 begins. + journal.commit_tx(); + + // --- Transaction 2 (same block, same journal): slot goes A -> 0 --- + let t2 = journal + .sstore(&mut db, addr, key, StorageValue::ZERO) + .unwrap() + .data; + + // Per EIP-2200, T2's "original value" must be A: that's what the slot + // would revert back to if *T2 alone* were reverted, since T1 already + // committed. Without the fix this was 0 (stale pre-block DB value), + // which would cause sstore_refund() to compute an incorrect gas + // refund for T2 (e.g. its is_original_eq_new() branch firing on + // 0 == 0, treating T2 as "restoring the original value" when it is + // not). + assert_eq!( + t2.original_value, value_a, + "T2's original_value is {:?}, expected {:?} (T1's committed value).", + t2.original_value, value_a + ); + } + + /// Same scenario, but modeling `ExecuteEvm::transact()`'s pattern: each + /// transaction gets its own `JournalInner` session that is finalized + /// (`finalize()`) and committed to a persistent outer `Database` + /// immediately after, rather than being reused via `commit_tx()` across + /// multiple transactions. This is `transact()` / `transact_commit()` + /// (finalize-per-tx), as opposed to `transact_many()` (one shared journal, + /// single finalize at the end). + #[test] + fn original_value_is_correct_when_each_tx_gets_its_own_finalized_session() { + use database::DatabaseCommit; + + let mut db = CacheDB::new(EmptyDB::new()); + + let addr = Address::with_last_byte(1); + let key = StorageKey::from(1); + let value_a = StorageValue::from(42); + + // --- Session 1 (transaction 1): fresh journal, 0 -> A --- + let mut journal1 = JournalInner::::new(); + journal1.load_account(&mut db, addr).unwrap(); + let t1 = journal1.sstore(&mut db, addr, key, value_a).unwrap().data; + assert_eq!(t1.original_value, StorageValue::ZERO); + // A real transaction touches every account it modifies (e.g. via + // deduct_caller / account-load accounting); CacheDB::commit() silently + // skips untouched accounts, so this is required for the diff to land. + journal1.touch(addr); + + journal1.commit_tx(); // handler.rs commits the single tx internally + let state1 = journal1.finalize(); // transact() finalizes right after + db.commit(state1); // caller commits the diff into the persistent DB + + // --- Session 2 (transaction 2): brand new journal/session, A -> 0 --- + let mut journal2 = JournalInner::::new(); + journal2.load_account(&mut db, addr).unwrap(); + let t2 = journal2 + .sstore(&mut db, addr, key, StorageValue::ZERO) + .unwrap() + .data; + + // Because this session's `state` map starts empty, the slot is loaded + // fresh from `db` (which already has T1's committed value), so + // original_value is correct here without needing any fix. + assert_eq!( + t2.original_value, value_a, + "original_value should be sourced fresh from the committed DB \ + value in the finalize-per-tx pattern, independent of the \ + commit_tx()-reuse bug." + ); + } +} + +#[cfg(test)] +mod eip6780_recreate_after_selfdestruct_tests { + use super::*; + use crate::JournalEntry; + use context_interface::journaled_state::TransferError; + use database::{CacheDB, DatabaseCommit, EmptyDB}; + use primitives::hardfork::SpecId; + + type TestJournal = JournalInner; + type TestDb = CacheDB; + + const SPEC: SpecId = SpecId::CANCUN; + + /// Sets up a journal + DB with a funded caller, ready to drive a sequence + /// of CREATE2/SELFDESTRUCT calls across multiple transactions sharing one + /// journal (revm's single-journal, multi-tx execution mode, e.g. + /// `transact_many()` - this is where all the bugs in this module were + /// found, since `transact()`'s finalize-per-tx pattern starts each + /// transaction with a fresh, empty `state` map). + fn setup() -> (TestJournal, TestDb, Address) { + let mut db = TestDb::new(EmptyDB::new()); + let mut journal = TestJournal::new(); + journal.set_spec_id(SPEC); + + let caller = Address::with_last_byte(1); + journal.load_account(&mut db, caller).unwrap(); + journal.state.get_mut(&caller).unwrap().info.balance = U256::from(1_000); + + (journal, db, caller) + } + + /// Creates a contract at `addr` within the current transaction, tagging + /// it with a distinct `code_hash` so tests can tell which "generation" + /// of the contract ends up surviving. + fn create_at( + journal: &mut TestJournal, + db: &mut TestDb, + caller: Address, + addr: Address, + code_hash: B256, + ) -> Result<(), TransferError> { + journal.load_account(db, addr).unwrap(); + journal.create_account_checkpoint(caller, addr, U256::ZERO, SPEC)?; + // Use the real, journaled code-setting path (as an actual CREATE would + // via set_code/set_code_with_hash) rather than poking info.code_hash + // directly - a direct field write wouldn't be journaled and so + // wouldn't be reverted by discard_tx()/checkpoint_revert(), which + // would be a test-harness artifact, not real behavior. + journal.set_code_with_hash(addr, Bytecode::default(), code_hash); + journal.checkpoint_commit(); + journal.touch(addr); + Ok(()) + } + + /// Selfdestructs the contract at `addr` within the current transaction. + fn destroy_at(journal: &mut TestJournal, db: &mut TestDb, addr: Address, target: Address) { + journal.selfdestruct(db, addr, target).unwrap(); + journal.touch(addr); + } + + const CODE_V1: B256 = B256::new([1; 32]); + const CODE_V2: B256 = B256::new([2; 32]); + const CODE_V3: B256 = B256::new([3; 32]); + + /// create -> destroy (same tx) -> recreate (later tx, no destroy). + /// + /// T1 creates a contract at `addr` (e.g. via CREATE2) and selfdestructs + /// it in the same transaction (EIP-6780 full-delete path). T1 commits. + /// T2, later in the *same block* (sharing this journal), creates a + /// contract at the *same* `addr` (e.g. same CREATE2 caller/salt/init-code) + /// and does NOT selfdestruct it. + /// + /// Expected: the account is alive at the end of the block - T2's + /// creation should be reflected in the final state. + #[test] + fn create_destroy_then_recreate_is_alive() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + // --- T1: create at `addr`, then selfdestruct it (same tx) --- + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); // T1 done, T2 begins + + // --- T2 (same block, same journal): recreate, do NOT destroy --- + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + journal.commit_tx(); // T2 done + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert!(final_account.is_created(), "sanity: account was created"); + assert!( + !final_account.is_selfdestructed(), + "account is still flagged globally selfdestructed even though \ + T2 legitimately recreated it afterward without destroying it \ + again" + ); + + // Demonstrate the real-world consequence: commit to a persistent DB + // and check whether the contract survives. + db.commit(state); + let committed = db.basic(addr).unwrap().expect("account should exist in DB"); + assert_eq!( + committed.code_hash, CODE_V2, + "T2's contract should be the one persisted, not wiped as destroyed" + ); + } + + /// create -> destroy -> recreate -> destroy (again, same tx as the + /// recreate). + /// + /// T1 creates and destroys `addr` (same tx). T2 recreates `addr` *and* + /// destroys it again, both within T2 (same-tx create+destroy, its own + /// independent EIP-6780 full-delete event). + /// + /// Expected: the account ends the block genuinely destroyed - the fix + /// must not make a same-tx create+destroy in T2 "stick alive" just + /// because an earlier transaction's destruction was un-marked. + #[test] + fn create_destroy_recreate_destroy_ends_destroyed() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + // --- T1: create then destroy (same tx) --- + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); + + // --- T2: recreate then destroy again, both in this same tx --- + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert!( + final_account.is_selfdestructed(), + "account should be destroyed: T2 created AND destroyed it in \ + the same transaction, an independent EIP-6780 full-delete event" + ); + } + + /// create (T1) -> selfdestruct in a *later*, unrelated transaction (T2). + /// + /// Per EIP-6780, since the SELFDESTRUCT happens in a different + /// transaction than the CREATE, this must NOT take the "same-tx full + /// delete" path - only the balance is transferred, code/storage survive. + #[test] + fn cross_tx_selfdestruct_does_not_fully_delete() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + let target = Address::with_last_byte(3); + + // --- T1: create, do NOT destroy --- + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + let balance = U256::from(1_000); + journal.balance_incr(&mut db, addr, balance).unwrap(); + journal.commit_tx(); + let acc = journal.state.get(&addr).unwrap(); + assert_eq!(acc.info.balance, balance, "balance should be non zero"); + + // --- T2: a later, unrelated tx selfdestructs it --- + journal.load_account(&mut db, addr).unwrap(); + destroy_at(&mut journal, &mut db, addr, target); + + let acc = journal.state.get(&addr).unwrap(); + assert!( + !acc.is_selfdestructed_locally(), + "cross-tx selfdestruct must not take the same-tx 'full delete' \ + path - it wasn't created in this transaction" + ); + assert_eq!( + acc.info.balance, + U256::ZERO, + "balance should be transferred out" + ); + assert_eq!( + acc.info.code_hash, CODE_V1, + "code must survive a cross-tx selfdestruct per EIP-6780" + ); + + let target_acc = journal.state.get(&target).unwrap(); + assert_eq!( + target_acc.info.balance, balance, + "the destroyed account's balance should land on the target address" + ); + } + + /// create (T1) -> cross-tx selfdestruct where the target IS the + /// destroyed account itself (T2, `address == target`). + /// + /// Per EIP-6780, a cross-tx (not same-tx-created) selfdestruct only + /// transfers balance to `target` - but transferring a balance to itself + /// is a no-op, so this whole call must leave the account completely + /// untouched: no balance change, no `SelfDestructed` flag set (not even + /// the global one), no journal entry pushed at all, code/storage intact. + /// This is a distinct EIP-6780 corner case (the `else { None }` branch + /// in `selfdestruct()`), separate from the single-journal recreate bug + /// this module otherwise covers - added specifically to confirm that + /// bug's fix (clearing `SelfDestructed`/`Created` in the lazy cold-load + /// wipe) has no effect on this unrelated, already-a-no-op path. + #[test] + fn cross_tx_self_targeting_selfdestruct_is_a_no_op() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + // --- T1: create, fund with a balance, do NOT destroy --- + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + let balance = U256::from(1_000); + journal.balance_incr(&mut db, addr, balance).unwrap(); + journal.commit_tx(); + + // --- T2: a later, unrelated tx selfdestructs it, targeting itself --- + journal.load_account(&mut db, addr).unwrap(); + let journal_len_before = journal.journal.len(); + journal.selfdestruct(&mut db, addr, addr).unwrap(); + + assert_eq!( + journal.journal.len(), + journal_len_before, + "a true no-op selfdestruct must not push any journal entry" + ); + + let acc = journal.state.get(&addr).unwrap(); + assert!( + !acc.is_selfdestructed_locally(), + "cross-tx self-targeting selfdestruct must not take the same-tx \ + 'full delete' path" + ); + assert!( + !acc.is_selfdestructed(), + "must not even mark globally selfdestructed - this is a no-op, \ + not a destruction" + ); + assert_eq!( + acc.info.balance, balance, + "balance must be completely unchanged - transferring to itself \ + is a no-op, not a zero-out" + ); + assert_eq!(acc.info.code_hash, CODE_V1, "code must survive untouched"); + } + + /// create (T1) -> cross-tx selfdestruct (T2) -> attempted recreate (T3). + /// + /// Once a contract survives past its creating transaction, EIP-6780 + /// means it is never actually removable from the trie by SELFDESTRUCT + /// again - so a later CREATE2 at the same address must still collide, + /// since the account still has code. This guards against the fix + /// over-reaching and allowing recreation where it shouldn't. + #[test] + fn cross_tx_destroyed_contract_cannot_be_recreated() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + let target = Address::with_last_byte(3); + + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + journal.commit_tx(); + + journal.load_account(&mut db, addr).unwrap(); + destroy_at(&mut journal, &mut db, addr, target); + journal.commit_tx(); + + let err = create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap_err(); + assert_eq!( + err, + TransferError::CreateCollision, + "an address whose code survived a cross-tx selfdestruct must \ + still be uncreatable" + ); + } + + /// create -> destroy (same tx) -> an intervening transaction merely + /// *touches* the account without recreating it -> a later transaction + /// recreates it. + /// + /// This specifically guards against an off-by-one timing bug: the fix + /// must clear the stale global `SelfDestructed` flag at the moment of + /// the lazy cold-load wipe itself, not deferred until whichever + /// transaction happens to touch the address next after a recreate. + #[test] + fn intervening_touch_then_later_recreate_is_alive() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); // T1 done + + // --- T2: just touches the address, no create, no destroy --- + journal.load_account(&mut db, addr).unwrap(); + journal.commit_tx(); // T2 done + + // --- T3: recreates it --- + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + journal.commit_tx(); // T3 done + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert!(final_account.is_created()); + assert!( + !final_account.is_selfdestructed(), + "an intervening transaction that only touched (didn't recreate) \ + the address must not prevent a later recreate from being seen \ + as alive" + ); + } + + /// Multiple full destroy/recreate cycles: create+destroy (T1) -> + /// recreate+destroy (T2, its own same-tx cycle) -> recreate only (T3). + /// + /// Stress-tests that the global flag is correctly cleared and re-set on + /// every cycle, not just the first one. + #[test] + fn multiple_full_destroy_recreate_cycles_end_alive() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); // T1: create+destroy + + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); // T2: recreate+destroy again + + create_at(&mut journal, &mut db, caller, addr, CODE_V3).unwrap(); + journal.commit_tx(); // T3: recreate, no destroy + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert!( + !final_account.is_selfdestructed(), + "after two full destroy/recreate cycles, a third recreate with \ + no further destroy must end up alive" + ); + assert_eq!( + final_account.info.code_hash, CODE_V3, + "the surviving contract should be T3's" + ); + } + + /// create -> destroy (T1, same tx) -> a *middle* transaction (T2) that + /// neither creates nor destroys, but writes a new balance to the dead + /// address -> recreate (T3). + /// + /// Real Ethereum semantics: sending value to a dead address resurrects a + /// plain, code-less account holding that balance; a later CREATE2 there + /// is still allowed (only code_hash/nonce gate the collision check, not + /// balance) and *adds* its value on top of what's already there rather + /// than overwriting it. This must hold the same way whether T1/T2/T3 + /// run as three separate finalize-per-tx sessions (each starting from a + /// clean slate re-read from the DB) or share one journal across + /// `commit_tx()` calls (where the account object, including its stale + /// global `Created` flag, is carried forward in memory). + #[test] + fn touch_with_new_balance_between_destroy_and_recreate_is_preserved() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + let extra_balance = U256::from(77); + + // --- T1: create then destroy (same tx) --- + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); + + // --- T2: neither creates nor destroys - just writes a new balance --- + journal.load_account(&mut db, addr).unwrap(); + let acc = journal.state.get_mut(&addr).unwrap(); + assert_eq!( + acc.info.code_hash, KECCAK_EMPTY, + "sanity: T1's destruction should already be physically wiped by \ + T2's cold-load, before T2 does anything itself" + ); + acc.info.balance += extra_balance; + journal.touch(addr); + journal.commit_tx(); + + // --- T3: recreates it --- + let create_value = U256::from(10); + journal.load_account(&mut db, addr).unwrap(); + journal + .create_account_checkpoint(caller, addr, create_value, SPEC) + .unwrap(); + journal.state.get_mut(&addr).unwrap().info.code_hash = CODE_V3; + journal.checkpoint_commit(); + journal.touch(addr); + journal.commit_tx(); + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert!(!final_account.is_selfdestructed()); + assert!(final_account.is_created()); + assert_eq!(final_account.info.code_hash, CODE_V3); + assert_eq!( + final_account.info.balance, + extra_balance + create_value, + "T3's creation must ADD its value on top of T2's balance, not \ + overwrite it" + ); + } + + /// A recreated contract's storage must read as zero (not a stale value + /// left over from the destroyed generation), and the first read of any + /// slot must be charged as a COLD access (EIP-2929) - not still-warm + /// from the destroyed generation's own writes in an earlier transaction. + #[test] + fn recreated_contract_storage_is_zero_and_cold() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + let key = StorageKey::from(7); + + // T1: create, write a nonzero value to `key`, destroy (same tx). + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + journal + .sstore(&mut db, addr, key, StorageValue::from(999)) + .unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); + + // T2: recreate (same address), do NOT write `key` again. + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + let sload_result = journal.sload(&mut db, addr, key).unwrap(); + + assert_eq!( + sload_result.data, + StorageValue::ZERO, + "recreated contract's storage must read as zero, not leak T1's value" + ); + assert!( + sload_result.is_cold, + "first read of this slot in T2 must be charged as a cold access" + ); + } + + /// T1 create+destroy (same tx), commits. T2 attempts to recreate the + /// same address but is then discarded (as a `ReadOnly` + /// automation-predicate call would be) rather than committed. T3, a + /// real (non-`ReadOnly`) transaction, then tries to recreate the same + /// address. T2's discarded attempt must leave no residue: it must not + /// block T3's recreate, and none of T2's (never-committed) contract may + /// leak into the final state. + #[test] + fn readonly_discarded_recreate_leaves_no_residue() { + let (mut journal, mut db, caller) = setup(); + let addr = Address::with_last_byte(2); + + create_at(&mut journal, &mut db, caller, addr, CODE_V1).unwrap(); + destroy_at(&mut journal, &mut db, addr, caller); + journal.commit_tx(); // T1 done + + // T2: attempts a recreate, but gets discarded (ReadOnly-style). + create_at(&mut journal, &mut db, caller, addr, CODE_V2).unwrap(); + journal.discard_tx(); // T2 discarded, not committed + + // T3: a real transaction, tries to recreate at the same address. + let t3_result = create_at(&mut journal, &mut db, caller, addr, CODE_V3); + assert!( + t3_result.is_ok(), + "T3 should be able to cleanly recreate where T2's discarded attempt failed" + ); + journal.commit_tx(); + + let state = journal.finalize(); + let final_account = state.get(&addr).unwrap().clone(); + assert_eq!( + final_account.info.code_hash, CODE_V3, + "only T3's contract should survive" + ); + assert!(!final_account.is_selfdestructed()); + } +} + +#[cfg(test)] +mod pre_cancun_selfdestruct_tests { + use super::*; + use crate::JournalEntry; + use database::EmptyDB; + use primitives::hardfork::SpecId; + use state::AccountInfo; + + /// Pre-Cancun, `selfdestruct()`'s `is_created_locally() || + /// !is_cancun_enabled` gate takes the "full delete" branch + /// unconditionally - i.e. for ANY destroy, not just one in the same + /// transaction as creation. So a cross-tx destroy of a pre-existing + /// contract (never created in this journal's history at all) still + /// sets `is_selfdestructed_locally()`. + /// + /// A later transaction in the same block merely *touching* (not + /// recreating) that address must NOT resurrect it by clearing the + /// global `SelfDestructed` flag: pre-Cancun, destruction is permanent, + /// and clearing that flag would let `apply_account_state`/ + /// `CacheDB::commit` skip wiping the account's committed storage from + /// the persisted state. + #[test] + fn pre_cancun_cross_tx_destroy_of_pre_existing_contract_stays_destroyed() { + let mut db = database::CacheDB::new(EmptyDB::new()); + let mut journal = JournalInner::::new(); + journal.set_spec_id(SpecId::BERLIN); + + let addr = Address::with_last_byte(2); + let target = Address::with_last_byte(3); + + // Pre-existing contract - NOT created in this journal's history, + // loaded straight from the DB with real code already in place. + db.insert_account_info( + addr, + AccountInfo { + code_hash: B256::from([9; 32]), + ..Default::default() + }, + ); + + // T1: destroy it (cross-tx relative to its own creation, which + // predates this block entirely). + journal.load_account(&mut db, addr).unwrap(); + journal.selfdestruct(&mut db, addr, target).unwrap(); + journal.touch(addr); + journal.commit_tx(); + + // Sanity: pre-Cancun, this must have taken the full-delete path. + assert!(journal.state.get(&addr).unwrap().is_selfdestructed()); + + // T2: a later transaction merely touches the address (e.g. a plain + // call), without recreating it. + journal.load_account(&mut db, addr).unwrap(); + journal.commit_tx(); + + assert!( + journal.state.get(&addr).unwrap().is_selfdestructed(), + "pre-Cancun, a cross-tx destroy of a pre-existing contract must \ + stay permanently destroyed - a later transaction merely \ + touching the address must not clear the global flag" + ); + } +} + +#[cfg(test)] +mod vacant_load_transaction_id_tests { + use super::*; + use crate::JournalEntry; + use database::{CacheDB, EmptyDB}; + use state::AccountInfo; + + /// A pre-existing (DB-loaded) account's first-ever load into a shared + /// journal must be stamped with the journal's real current + /// `transaction_id`, not a hardcoded 0 (the bug: `From for + /// Account`, `crates/state/src/lib.rs`, hardcoded `transaction_id: 0`). + /// Otherwise `mark_warm_with_transaction_id` misreads this account's + /// very next touch, later in the SAME transaction, as a brand-new + /// transaction touching it for the first time, charging it cold + /// instead of warm. This is the general form of the bug that also + /// manifests as `EXTCODESIZE(CALLER)` being incorrectly charged cold - + /// see `test_sender_extcodesize_stays_warm_after_prior_committed_tx` in + /// `crates/ee-tests/src/revm_tests.rs`. + #[test] + fn pre_existing_account_first_load_stamps_real_transaction_id() { + let mut db = CacheDB::new(EmptyDB::new()); + let unrelated = Address::with_last_byte(1); + let target = Address::with_last_byte(2); + + db.insert_account_info( + unrelated, + AccountInfo { + balance: U256::from(1), + ..Default::default() + }, + ); + db.insert_account_info( + target, + AccountInfo { + balance: U256::from(2), + ..Default::default() + }, + ); + + let mut journal = JournalInner::::new(); + + // T0: an unrelated transaction, never touching `target`, commits + // first - advancing transaction_id past 0. + journal.load_account(&mut db, unrelated).unwrap(); + journal.commit_tx(); + assert_eq!(journal.transaction_id, 1); + + // T1: first-ever load of `target` in this journal - a different, + // non-first transaction. + journal.load_account(&mut db, target).unwrap(); + + assert_eq!( + journal.state.get(&target).unwrap().transaction_id, + journal.transaction_id, + "a pre-existing account's first load must be stamped with the \ + journal's real current transaction_id, not a hardcoded 0" + ); + + // Concretely: touching it again right now, still within T1, must + // report warm (is_cold == false) - not misread as a new + // transaction's first touch. + let second_touch = journal.load_account(&mut db, target).unwrap(); + assert!( + !second_touch.is_cold, + "second touch of the same account within the same transaction \ + must be warm" + ); + } +} + +#[cfg(test)] +mod eip6780_selfdestruct_burn_tests { + use super::*; + use crate::JournalEntry; + use database::{CacheDB, EmptyDB}; + use primitives::hardfork::SpecId; + + type TestJournal = JournalInner; + type TestDb = CacheDB; + + const CALLER: Address = Address::with_last_byte(1); + const CONTRACT: Address = Address::with_last_byte(2); + const BENEFICIARY: Address = Address::with_last_byte(3); + const ENDOWMENT: u64 = 500; + + /// A journal with a funded caller, ready to create and destroy contracts. + fn setup(spec: SpecId) -> (TestJournal, TestDb) { + let mut db = TestDb::new(EmptyDB::new()); + let mut journal = TestJournal::new(); + journal.set_spec_id(spec); + journal.load_account(&mut db, CALLER).unwrap(); + journal.state.get_mut(&CALLER).unwrap().info.balance = U256::from(1_000); + (journal, db) + } + + /// Creates a contract at `CONTRACT` in the current transaction, endowed from + /// `CALLER`, and commits the creation checkpoint. + fn create_endowed_contract(journal: &mut TestJournal, db: &mut TestDb, spec: SpecId) { + journal.load_account(db, CONTRACT).unwrap(); + journal + .create_account_checkpoint(CALLER, CONTRACT, U256::from(ENDOWMENT), spec) + .unwrap(); + journal.checkpoint_commit(); + journal.touch(CONTRACT); + } + + /// A contract created in this transaction that names itself as beneficiary has + /// its balance zeroed with no credit anywhere: the value is destroyed and must + /// be reported. + #[test] + fn same_tx_self_targeting_selfdestruct_reports_the_balance() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + + assert_eq!( + journal.selfdestruct_burn, + U256::from(ENDOWMENT), + "a same-tx self-targeting selfdestruct destroys the whole balance" + ); + assert_eq!( + journal.state.get(&CONTRACT).unwrap().info.balance, + U256::ZERO + ); + } + + /// Naming a different beneficiary moves the value instead of destroying it, so + /// nothing may be reported as burned. + #[test] + fn same_tx_selfdestruct_to_another_address_reports_nothing() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + + journal + .selfdestruct(&mut db, CONTRACT, BENEFICIARY) + .unwrap(); + + assert_eq!( + journal.selfdestruct_burn, + U256::ZERO, + "value credited to another account is moved, not destroyed" + ); + assert_eq!( + journal.state.get(&BENEFICIARY).unwrap().info.balance, + U256::from(ENDOWMENT) + ); + } + + /// The reported total must follow the same revert rules as the state itself: a + /// frame that is reverted destroyed nothing. + #[test] + fn reverted_frame_reports_nothing() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + + let checkpoint = journal.checkpoint(); + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + assert_eq!(journal.selfdestruct_burn, U256::from(ENDOWMENT)); + + journal.checkpoint_revert(checkpoint); + + assert_eq!( + journal.selfdestruct_burn, + U256::ZERO, + "a reverted selfdestruct destroyed nothing and must not be reported" + ); + assert_eq!( + journal.state.get(&CONTRACT).unwrap().info.balance, + U256::from(ENDOWMENT), + "the reverted balance is restored, so it is still in circulation" + ); + } + + /// An outer frame reverting a *nested* frame's burn must also unreport it, and + /// must leave an earlier committed burn alone. + #[test] + fn nested_revert_keeps_only_the_committed_burn() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + + // Committed burn: the whole endowment. + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + + // A second contract is created and destroyed inside a frame that reverts. + let outer = journal.checkpoint(); + let second = Address::with_last_byte(4); + journal.load_account(&mut db, second).unwrap(); + journal + .create_account_checkpoint(CALLER, second, U256::from(7), SpecId::CANCUN) + .unwrap(); + journal.checkpoint_commit(); + journal.checkpoint(); + journal.selfdestruct(&mut db, second, second).unwrap(); + journal.checkpoint_commit(); + assert_eq!(journal.selfdestruct_burn, U256::from(ENDOWMENT + 7)); + + journal.checkpoint_revert(outer); + + assert_eq!( + journal.selfdestruct_burn, + U256::from(ENDOWMENT), + "only the burn that survived the revert is reported" + ); + } + + /// A discarded transaction reverts every entry, so it reports nothing. + #[test] + fn discarded_transaction_reports_nothing() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + assert_eq!(journal.selfdestruct_burn, U256::from(ENDOWMENT)); + + journal.discard_tx(); + + assert_eq!( + journal.selfdestruct_burn, + U256::ZERO, + "a discarded transaction destroyed nothing" + ); + } + + /// Before Cancun every selfdestruct takes the destroying branch, so a + /// self-targeting one burns even when the account was not created in this + /// transaction. + #[test] + fn pre_cancun_self_targeting_selfdestruct_reports_the_balance() { + let (mut journal, mut db) = setup(SpecId::SHANGHAI); + journal.load_account(&mut db, CONTRACT).unwrap(); + journal.state.get_mut(&CONTRACT).unwrap().info.balance = U256::from(ENDOWMENT); + + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + + assert_eq!( + journal.selfdestruct_burn, + U256::from(ENDOWMENT), + "pre-Cancun a self-targeting selfdestruct destroys the balance" + ); + } + + /// Before Cancun a selfdestruct to a different beneficiary still only moves the + /// value, so it is not a burn. + #[test] + fn pre_cancun_selfdestruct_to_another_address_reports_nothing() { + let (mut journal, mut db) = setup(SpecId::SHANGHAI); + journal.load_account(&mut db, CONTRACT).unwrap(); + journal.state.get_mut(&CONTRACT).unwrap().info.balance = U256::from(ENDOWMENT); + + journal + .selfdestruct(&mut db, CONTRACT, BENEFICIARY) + .unwrap(); + + assert_eq!(journal.selfdestruct_burn, U256::ZERO); + assert_eq!( + journal.state.get(&BENEFICIARY).unwrap().info.balance, + U256::from(ENDOWMENT) + ); + } + + /// After Cancun, a contract that was not created in this transaction and names + /// itself as beneficiary is a no-op: no entry, no state change, no burn. + #[test] + fn cross_tx_self_targeting_selfdestruct_reports_nothing() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + journal.commit_tx(); + // A cold reload in the next transaction clears the local "created" flag. + journal.load_account(&mut db, CONTRACT).unwrap(); + + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + + assert_eq!( + journal.selfdestruct_burn, + U256::ZERO, + "an EIP-6780 no-op selfdestruct destroys nothing" + ); + assert_eq!( + journal.state.get(&CONTRACT).unwrap().info.balance, + U256::from(ENDOWMENT), + "the balance is untouched" + ); + } + + /// Draining part-way through a transaction breaks the accumulator's contract, + /// because a later revert then has nothing left to take back. Debug builds catch + /// that at the revert site; release builds saturate instead, so that a broken + /// caller costs accounting accuracy rather than halting the node. + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "drained mid-transaction")] + fn draining_mid_transaction_is_caught_in_debug_builds() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + + let checkpoint = journal.checkpoint(); + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + // The contract violation: the burn is taken while its journal entry is still + // revertible. + assert_eq!(journal.take_selfdestruct_burn(), U256::from(ENDOWMENT)); + + journal.checkpoint_revert(checkpoint); + } + + /// The total survives the transaction boundary and `finalize`, so the host can + /// read it after execution, and draining it resets it. + #[test] + fn total_survives_finalize_and_is_drained_by_take() { + let (mut journal, mut db) = setup(SpecId::CANCUN); + create_endowed_contract(&mut journal, &mut db, SpecId::CANCUN); + journal.selfdestruct(&mut db, CONTRACT, CONTRACT).unwrap(); + + journal.commit_tx(); + assert_eq!(journal.selfdestruct_burn, U256::from(ENDOWMENT)); + let _ = journal.finalize(); + assert_eq!(journal.selfdestruct_burn, U256::from(ENDOWMENT)); + + assert_eq!(journal.take_selfdestruct_burn(), U256::from(ENDOWMENT)); + assert_eq!(journal.selfdestruct_burn, U256::ZERO); + } +} diff --git a/crates/context/src/tx.rs b/crates/context/src/tx.rs index d3510e5c6c..2975b50f6d 100644 --- a/crates/context/src/tx.rs +++ b/crates/context/src/tx.rs @@ -86,6 +86,8 @@ pub struct TxEnv { /// /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 pub authorization_list: Vec>, + /// Transaction hash + pub tx_hash: B256, } impl Default for TxEnv { @@ -226,6 +228,10 @@ impl Transaction for TxEnv { fn max_priority_fee_per_gas(&self) -> Option { self.gas_priority_fee } + + fn tx_hash(&self) -> B256 { + self.tx_hash + } } /// Builder for constructing [`TxEnv`] instances @@ -245,6 +251,7 @@ pub struct TxEnvBuilder { blob_hashes: Vec, max_fee_per_blob_gas: u128, authorization_list: Vec>, + tx_hash: B256, } impl TxEnvBuilder { @@ -265,6 +272,7 @@ impl TxEnvBuilder { blob_hashes: Vec::new(), max_fee_per_blob_gas: 0, authorization_list: Vec::new(), + tx_hash: B256::ZERO, } } @@ -374,6 +382,12 @@ impl TxEnvBuilder { self } + /// Set the transaction hash + pub fn tx_hash(mut self, tx_hash: B256) -> Self { + self.tx_hash = tx_hash; + self + } + /// Set the authorization list pub fn authorization_list( mut self, @@ -473,6 +487,7 @@ impl TxEnvBuilder { blob_hashes: self.blob_hashes, max_fee_per_blob_gas: self.max_fee_per_blob_gas, authorization_list: self.authorization_list, + tx_hash: self.tx_hash, }; // if tx_type is not set, derive it from fields and fix errors. @@ -565,6 +580,7 @@ impl TxEnvBuilder { blob_hashes: self.blob_hashes, max_fee_per_blob_gas: self.max_fee_per_blob_gas, authorization_list: self.authorization_list, + tx_hash: self.tx_hash, }; // Derive tx type from fields, if some fields are wrongly set it will return an error. @@ -626,6 +642,7 @@ impl TxEnv { blob_hashes, max_fee_per_blob_gas, authorization_list, + tx_hash, } = self; TxEnvBuilder::new() @@ -643,6 +660,7 @@ impl TxEnv { .blob_hashes(blob_hashes) .max_fee_per_blob_gas(max_fee_per_blob_gas) .authorization_list(authorization_list) + .tx_hash(tx_hash) } } diff --git a/crates/ee-tests/src/access_list_warmth.rs b/crates/ee-tests/src/access_list_warmth.rs new file mode 100644 index 0000000000..6344eeb6f3 --- /dev/null +++ b/crates/ee-tests/src/access_list_warmth.rs @@ -0,0 +1,104 @@ +use revm::{ + context::ContextTr, + context::TxEnv, + context_interface::transaction::{AccessList, AccessListItem, TransactionType}, + database::{CacheDB, EmptyDB}, + primitives::{address, hardfork::SpecId, Bytes, TxKind, U256}, + state::AccountInfo, + Context, ExecuteEvm, MainBuilder, MainContext, +}; + +/// Regression test: EIP-2930 access lists pre-warm addresses/storage keys +/// for the *current transaction only*. In revm's single-journal, multi-tx +/// execution mode, an access-listed address gets inserted into the shared +/// `state` map by the transaction that declares it - this test confirms +/// that insertion does not leak warmth into a *later*, unrelated +/// transaction that does not itself declare the address in its own access +/// list (unlike the COINBASE/precompile case, access-listed addresses are +/// never added to the perpetually-pre-warmed `warm_addresses` set, so the +/// fix for the COINBASE gas deviation must not - and does not - affect this +/// path). +#[test] +fn access_list_warmth_does_not_leak_into_later_tx() { + let mut db = CacheDB::new(EmptyDB::default()); + + let caller = address!("1000000000000000000000000000000000000000"); + let listed_addr = address!("5555555555555555555555555555555555555555"); + let contract = address!("3000000000000000000000000000000000000000"); + + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(10_u128.pow(18)), + ..Default::default() + }, + ); + + let mut code = vec![0x73]; // PUSH20 + code.extend_from_slice(listed_addr.as_slice()); + code.push(0x31); // BALANCE + code.push(0x00); // STOP + + db.insert_account_info( + contract, + AccountInfo { + code_hash: revm::primitives::keccak256(&code), + code: Some(revm::bytecode::Bytecode::new_raw(Bytes::from(code))), + ..Default::default() + }, + ); + + let mut evm = Context::mainnet() + .with_db(db) + .modify_cfg_chained(|cfg| cfg.spec = SpecId::PRAGUE) + .build_mainnet(); + + // T1: access list includes `listed_addr` - pre-warms it for T1 only. + let tx1 = TxEnv { + caller, + kind: TxKind::Call(contract), + gas_limit: 1_000_000, + tx_type: TransactionType::Eip2930 as u8, + access_list: AccessList(vec![AccessListItem { + address: listed_addr, + storage_keys: vec![], + }]), + ..Default::default() + }; + + let result1 = evm.transact_one(tx1).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + // T2: plain legacy tx, no access list, same BALANCE(listed_addr) call. + let tx2 = TxEnv { + caller, + kind: TxKind::Call(contract), + gas_limit: 1_000_000, + nonce: 1, + ..Default::default() + }; + + let result2 = evm.transact_one(tx2).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + // T2 must pay the full cold-access cost for `listed_addr` - identical to + // an ordinary address that was never access-listed by anyone (23603, + // per the `ordinary_address_is_cold_every_tx` sibling test) - proving + // T1's access list left no residual warmth behind. + assert_eq!( + result2.gas_used(), + 23603, + "T2 (no access list of its own) must pay the full cold access cost for \ + `listed_addr`, not inherit warmth from T1's access list" + ); + + // T1 itself should be cheaper than a cold access by exactly 100 gas: + // -2500 (BALANCE opcode now warm instead of cold) + 2400 (EIP-2930's + // upfront per-address access-list declaration cost) = -100. + assert_eq!( + result1.gas_used(), + 23503, + "T1's own access list should make its BALANCE access warm, net of the \ + upfront access-list declaration cost" + ); +} diff --git a/crates/ee-tests/src/coinbase_warmth.rs b/crates/ee-tests/src/coinbase_warmth.rs new file mode 100644 index 0000000000..1bc08191fc --- /dev/null +++ b/crates/ee-tests/src/coinbase_warmth.rs @@ -0,0 +1,130 @@ +use revm::{ + context::ContextTr, + context::TxEnv, + database::{CacheDB, EmptyDB}, + primitives::{address, hardfork::SpecId, Address, Bytes, TxKind, U256}, + state::AccountInfo, + Context, ExecuteEvm, MainBuilder, MainContext, +}; + +/// Deploys `PUSH20 ; BALANCE; STOP` - a contract whose only job is +/// to report the gas cost of a single BALANCE access to `target`. +fn balance_check_bytecode(target: Address) -> Vec { + let mut code = vec![0x73]; // PUSH20 + code.extend_from_slice(target.as_slice()); + code.push(0x31); // BALANCE + code.push(0x00); // STOP + code +} + +fn setup_evm( + caller: Address, + contract: Address, + balance_target: Address, +) -> revm::MainnetEvm< + Context>, +> { + let mut db = CacheDB::new(EmptyDB::default()); + + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(10_u128.pow(18)), + ..Default::default() + }, + ); + + let code = balance_check_bytecode(balance_target); + db.insert_account_info( + contract, + AccountInfo { + code_hash: revm::primitives::keccak256(&code), + code: Some(revm::bytecode::Bytecode::new_raw(Bytes::from(code))), + ..Default::default() + }, + ); + + Context::mainnet() + .with_db(db) + .modify_cfg_chained(|cfg| cfg.spec = SpecId::PRAGUE) + .build_mainnet() +} + +/// Regression test for the reviewer-reported gas deviation: EIP-3651 makes +/// COINBASE warm at the start of *every* transaction. In revm's +/// single-journal, multi-tx execution mode (`commit_tx()` between +/// transactions sharing one journal), the coinbase account gets inserted +/// into `state` by the first transaction that touches it, and every later +/// transaction's access went through `load_account_optional`'s `Occupied` +/// branch - which only checked the account's own stale `transaction_id` +/// stamp, never the perpetually-pre-warmed `warm_addresses` set. So every +/// transaction after the first paid a full cold access (2600 gas) for +/// COINBASE instead of the warm 100 gas EIP-3651 guarantees. +#[test] +fn coinbase_stays_warm_across_consecutive_txs() { + let caller = address!("1000000000000000000000000000000000000000"); + let coinbase = Address::ZERO; + let contract = address!("3000000000000000000000000000000000000000"); + + let mut evm = setup_evm(caller, contract, coinbase); + + let mut tx = TxEnv { + caller, + kind: TxKind::Call(contract), + ..Default::default() + }; + + let result1 = evm.transact_one(tx.clone()).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + tx.nonce = 1; + let result2 = evm.transact_one(tx).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + assert_eq!( + result1.gas_used(), + result2.gas_used(), + "gas used must be identical for two identical contract calls in the same block - \ + COINBASE must be warm at the start of every transaction, per EIP-3651" + ); +} + +/// Companion to the test above: guards against an over-broad fix. Ordinary +/// addresses (not COINBASE, not a precompile) must still correctly reset to +/// cold at the start of every transaction, per standard EIP-2929 semantics - +/// warmth from an earlier transaction touching the same address must not +/// leak into a later one just because the account object persists in the +/// shared journal's `state` map. +#[test] +fn ordinary_address_is_cold_every_tx() { + let caller = address!("1000000000000000000000000000000000000000"); + let ordinary = address!("4444444444444444444444444444444444444444"); + let contract = address!("3000000000000000000000000000000000000000"); + + let mut evm = setup_evm(caller, contract, ordinary); + + let mut tx = TxEnv { + caller, + kind: TxKind::Call(contract), + ..Default::default() + }; + + let result1 = evm.transact_one(tx.clone()).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + tx.nonce = 1; + let result2 = evm.transact_one(tx).unwrap(); + evm.ctx.journal_mut().commit_tx(); + + assert_eq!( + result1.gas_used(), + result2.gas_used(), + "an ordinary address should cost the same (cold) in both transactions" + ); + assert_eq!( + result1.gas_used(), + 23603, + "sanity: this must be the COLD BALANCE cost, not the warm one - otherwise this test \ + would pass even if the fix incorrectly made every address stay warm across transactions" + ); +} diff --git a/crates/ee-tests/src/lib.rs b/crates/ee-tests/src/lib.rs index e07453abcf..c8b804e31b 100644 --- a/crates/ee-tests/src/lib.rs +++ b/crates/ee-tests/src/lib.rs @@ -132,3 +132,9 @@ mod op_revm_tests; #[cfg(test)] mod revm_tests; + +#[cfg(test)] +mod coinbase_warmth; + +#[cfg(test)] +mod access_list_warmth; diff --git a/crates/ee-tests/src/revm_tests.rs b/crates/ee-tests/src/revm_tests.rs index b89459ebfc..f60322c5e1 100644 --- a/crates/ee-tests/src/revm_tests.rs +++ b/crates/ee-tests/src/revm_tests.rs @@ -4,6 +4,10 @@ use crate::TestdataConfig; use revm::{ bytecode::opcode, context::{ContextTr, TxEnv}, + context_interface::{ + cfg::ExecutionMode, + transaction::{Authorization, RecoveredAuthority, RecoveredAuthorization, TransactionType}, + }, database::{BenchmarkDB, BENCH_CALLER, BENCH_TARGET}, primitives::{address, b256, hardfork::SpecId, Bytes, TxKind, KECCAK_EMPTY, U256}, state::{AccountStatus, Bytecode}, @@ -78,6 +82,259 @@ fn test_selfdestruct_multi_tx() { ); } +const STOP_BYTECODE: &[u8] = &[opcode::STOP]; + +/// Regression test for a bug where a later `ExecutionMode::ReadOnly` transaction's +/// `discard_tx()` could erase the `Touched` status a prior, already-committed transaction had +/// set on the same account (e.g. the caller of an automation-task predicate check that runs +/// after the same account's own ordinary transaction earlier in the block). Because +/// `Touched` is a sticky, block-scoped flag consumed by state-diff builders (e.g. +/// `CacheState::apply_account_state`) to decide whether an account appears in the persisted +/// output at all, incorrectly clearing it silently drops the prior transaction's committed +/// nonce/balance change from the output. +#[test] +fn test_read_only_discard_does_not_revert_prior_committed_tx() { + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::CANCUN) + .with_db(BenchmarkDB::new_bytecode(Bytecode::new_legacy( + STOP_BYTECODE.into(), + ))) + .build_mainnet(); + + // T1: an ordinary user transaction from BENCH_CALLER. Bumps its nonce, deducts gas from its + // balance, and commits normally. + let result1 = evm + .transact_one(TxEnv::builder_for_bench().build_fill()) + .unwrap(); + assert!(result1.is_success()); + + let caller_after_t1 = evm + .ctx + .journal_mut() + .state + .get(&BENCH_CALLER) + .unwrap() + .clone(); + assert!(caller_after_t1.is_touched()); + assert_eq!(caller_after_t1.info.nonce, 1); + + // T2: same caller, executed in ReadOnly mode (as an automation-task predicate check would + // be). ReadOnly mode does not charge gas or bump the nonce, and this call performs no state + // mutations, so it succeeds and its journal is discarded rather than erroring. + evm.ctx + .modify_cfg(|cfg| cfg.execution_mode = ExecutionMode::ReadOnly); + let result2 = evm + .transact_one(TxEnv::builder_for_bench().nonce(1).build_fill()) + .unwrap(); + assert!(result2.is_success()); + + let caller_after_t2 = evm.ctx.journal_mut().state.get(&BENCH_CALLER).unwrap(); + + // T2 must have been fully discarded: nonce/balance stay exactly as T1 committed them. + assert_eq!(caller_after_t2.info.nonce, caller_after_t1.info.nonce); + assert_eq!(caller_after_t2.info.balance, caller_after_t1.info.balance); + + // The critical assertion: T1's committed touch must survive T2's ReadOnly discard. + assert!( + caller_after_t2.is_touched(), + "T1's committed AccountTouched status for the caller must survive a later ReadOnly \ + transaction's discard_tx(); otherwise state-diff builders skip the account entirely \ + and T1's committed nonce/balance change is lost" + ); + + // The account must still show up (with T1's values) once the batch is finalized. + let output = evm.finalize(); + let caller_output = output.get(&BENCH_CALLER).unwrap(); + assert!(caller_output.is_touched()); + assert_eq!(caller_output.info.nonce, 1); +} + +/// Regression test for the flip side of the above fix: an account whose *only* interaction in +/// a block is itself a discarded transaction (e.g. a `ReadOnly` predicate check on an account +/// that has not otherwise appeared in this block) must end up untouched, not spuriously touched. +#[test] +fn test_read_only_discard_of_first_touch_is_fully_undone() { + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| { + cfg.spec = SpecId::CANCUN; + cfg.execution_mode = ExecutionMode::ReadOnly; + }) + .with_db(BenchmarkDB::new_bytecode(Bytecode::new_legacy( + STOP_BYTECODE.into(), + ))) + .build_mainnet(); + + // BENCH_CALLER has never been touched before; this ReadOnly transaction is its first-ever + // interaction this block, and it performs no mutations, so it is discarded. + let result = evm + .transact_one(TxEnv::builder_for_bench().build_fill()) + .unwrap(); + assert!(result.is_success()); + + let caller_after = evm.ctx.journal_mut().state.get(&BENCH_CALLER).unwrap(); + assert!( + !caller_after.is_touched(), + "an account whose only interaction in the block was a discarded ReadOnly transaction \ + must end up untouched, not leak a spurious touch into the state diff" + ); +} + +/// Regression test: `apply_eip7702_auth_list` mutates the authority account's +/// `info.code`/`info.code_hash`/`info.nonce` directly. In `ExecutionMode::ReadOnly` +/// (e.g. an automation-task predicate check), these mutations must be fully +/// journaled so that: +/// 1. `has_state_mutations()` detects the attempt (rather than silently missing +/// it, since it only inspects journal entries), causing `transact_one` to +/// return an error instead of succeeding. +/// 2. `discard_tx()` fully reverts the authority's code/nonce/touch, so nothing +/// leaks into subsequent transactions sharing this journal. +#[test] +fn test_read_only_eip7702_auth_list_is_fully_discarded() { + let authority = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let delegate_to = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| { + cfg.spec = SpecId::PRAGUE; + cfg.execution_mode = ExecutionMode::ReadOnly; + }) + .with_db(BenchmarkDB::new_bytecode(Bytecode::new_legacy( + STOP_BYTECODE.into(), + ))) + .build_mainnet(); + + let auth = RecoveredAuthorization::new_unchecked( + Authorization { + // Zero chain_id is always accepted, regardless of the context's actual chain id. + chain_id: U256::ZERO, + address: delegate_to, + nonce: 0, + }, + RecoveredAuthority::Valid(authority), + ); + + let tx = TxEnv::builder_for_bench() + .tx_type(Some(TransactionType::Eip7702 as u8)) + .authorization_list_recovered(vec![auth]) + .build_fill(); + + // Applying the authorization list mutates the authority's code/nonce - this + // must be detected as a ReadOnly state-mutation attempt, not allowed to + // silently succeed. + let err = evm.transact_one(tx).unwrap_err(); + assert!( + err.to_string().contains("attempted state mutation"), + "expected a ReadOnly state-mutation error, got: {err}" + ); + + // And the authority account must show no trace of the attempted delegation: + // discard_tx() must have fully reverted the code/nonce/touch mutations that + // apply_eip7702_auth_list made before last_frame_result detected them. + if let Some(authority_after) = evm.ctx.journal_mut().state.get(&authority) { + assert_eq!(authority_after.info.nonce, 0, "nonce bump must be reverted"); + assert_eq!( + authority_after.info.code_hash, KECCAK_EMPTY, + "code delegation must be reverted" + ); + assert!( + !authority_after.is_touched(), + "the authority's touch must not leak from a discarded ReadOnly transaction" + ); + } +} + +/// Regression test: `JournalEntry::CodeChange`'s revert previously always +/// reset the account to `code_hash = KECCAK_EMPTY, code = None` - correct +/// for CREATE (whose collision check requires the target to already be +/// empty), but wrong for EIP-7702: step 5 of the spec explicitly permits +/// re-delegating an authority that already holds a delegation, so the +/// "previous" code being reverted-to is not necessarily empty. +/// +/// T1 (committed) delegates `authority` to X. T2 (`ReadOnly`) attempts to +/// re-delegate the *same* authority to Y, correctly errors as a state +/// mutation attempt, and gets discarded. T1's delegation to X must survive +/// - not be wiped to empty by T2's reverted `CodeChange` entry. +#[test] +fn test_read_only_eip7702_redelegate_restores_prior_delegation_on_discard() { + let authority = address!("0xcccccccccccccccccccccccccccccccccccccccc"); + let delegate_to_x = address!("0xdddddddddddddddddddddddddddddddddddddddd"); + let delegate_to_y = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::PRAGUE) + .with_db(BenchmarkDB::new_bytecode(Bytecode::new_legacy( + STOP_BYTECODE.into(), + ))) + .build_mainnet(); + + // T1 (normal, committed): delegate `authority` to X. + let auth1 = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::ZERO, + address: delegate_to_x, + nonce: 0, + }, + RecoveredAuthority::Valid(authority), + ); + let tx1 = TxEnv::builder_for_bench() + .tx_type(Some(TransactionType::Eip7702 as u8)) + .authorization_list_recovered(vec![auth1]) + .build_fill(); + + let result1 = evm.transact_one(tx1).unwrap(); + assert!(result1.is_success()); + + let authority_after_t1 = evm.ctx.journal_mut().state.get(&authority).unwrap().clone(); + assert_eq!( + authority_after_t1.info.nonce, 1, + "sanity: T1's delegation bumped the authority's nonce" + ); + assert_ne!( + authority_after_t1.info.code_hash, KECCAK_EMPTY, + "sanity: T1's delegation to X must be committed before T2 runs" + ); + + // T2 (ReadOnly): attempt to re-delegate the SAME authority to Y. + evm.ctx + .modify_cfg(|cfg| cfg.execution_mode = ExecutionMode::ReadOnly); + + let auth2 = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::ZERO, + address: delegate_to_y, + nonce: 1, + }, + RecoveredAuthority::Valid(authority), + ); + let tx2 = TxEnv::builder_for_bench() + .nonce(1) + .tx_type(Some(TransactionType::Eip7702 as u8)) + .authorization_list_recovered(vec![auth2]) + .build_fill(); + + let err = evm.transact_one(tx2).unwrap_err(); + assert!( + err.to_string().contains("attempted state mutation"), + "expected a ReadOnly state-mutation error, got: {err}" + ); + + // T1's committed delegation to X must survive T2's discarded + // re-delegation attempt - not be wiped to empty. + let authority_after_t2 = evm.ctx.journal_mut().state.get(&authority).unwrap(); + assert_eq!( + authority_after_t2.info.nonce, 1, + "T2's nonce bump must be reverted, leaving T1's committed nonce" + ); + assert_eq!( + authority_after_t2.info.code_hash, authority_after_t1.info.code_hash, + "T1's committed delegation to X must be restored, not wiped to empty" + ); + assert_eq!( + authority_after_t2.info.code, authority_after_t1.info.code, + "T1's committed delegation bytecode must be restored" + ); +} + /// Tests multiple transactions with contract creation. /// Verifies that created contracts persist correctly across transactions /// and that their state is properly maintained. @@ -173,12 +430,14 @@ fn test_multi_tx_create() { .get_mut(&created_address) .unwrap(); + // T3 recreated the contract at the same address without destroying it again, + // so the stale `SelfDestructed` flag from T2's destruction must be cleared - + // see the EIP-6780 recreate-after-selfdestruct fix. assert_eq!( created_acc.status, AccountStatus::Created | AccountStatus::CreatedLocal | AccountStatus::Touched - | AccountStatus::SelfDestructed | AccountStatus::LoadedAsNotExisting ); let output = evm.finalize(); @@ -283,3 +542,228 @@ fn test_disable_balance_check() { let expected_balance = U256::ZERO; assert_eq!(returned_balance, expected_balance); } + +/// Funds `addr` with a large balance in a fresh `CacheDB`. +fn funded_cache_db( + addr: revm::primitives::Address, +) -> revm::database::CacheDB { + use revm::database::{CacheDB, EmptyDB}; + use revm::state::AccountInfo; + + let mut db = CacheDB::new(EmptyDB::new()); + db.insert_account_info( + addr, + AccountInfo { + balance: U256::from(10_000_000_000_000_000_000u128), + ..Default::default() + }, + ); + db +} + +/// T1(create->destroy) -> T2(call the destroyed contract), single shared journal +/// (`transact_one` twice on the same `Evm`, i.e. revm's single-journal, multi-tx +/// execution mode). T1's init code is `PUSH2 0xFFFF; SELFDESTRUCT; STOP` - the +/// contract destroys itself in its own constructor, so it is fully deleted per +/// EIP-6780 (same-tx create+destroy) without ever having runtime code. T2 then +/// calls that address as if invoking a function on it. +/// +/// Also asserts the account's *internal bookkeeping* (`AccountStatus`) ends up +/// identical to the standalone-journal version of the same scenario below - +/// see the fix in `load_account_optional`'s lazy cold-load wipe, which now +/// clears the global `Created` flag alongside `SelfDestructed` so a +/// shared-journal execution doesn't retain a stale flag that a fresh, +/// finalize-per-tx session would never have set in the first place. +#[test] +fn test_call_after_create_destroy_single_journal() { + let db = funded_cache_db(BENCH_CALLER); + + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::CANCUN) + .with_db(db) + .build_mainnet(); + + let result1 = evm + .transact_one( + TxEnv::builder_for_bench() + .kind(TxKind::Create) + .data(Bytes::copy_from_slice(SELFDESTRUCT_BYTECODE)) + .build_fill(), + ) + .unwrap(); + assert!(result1.is_success()); + let created_address = result1.created_address().unwrap(); + + // Sanity: the constructor genuinely selfdestructed (not just deployed empty code). + assert!(evm + .ctx + .journal_mut() + .state + .get(&created_address) + .unwrap() + .is_selfdestructed_locally()); + + let result2 = evm + .transact_one( + TxEnv::builder_for_bench() + .nonce(1) + .kind(TxKind::Call(created_address)) + .data(Bytes::from_static(&[0xaa, 0xbb, 0xcc, 0xdd])) + .build_fill(), + ) + .unwrap(); + + // Calling a destroyed contract must be a harmless no-op: no code left to run. + assert!(result2.is_success()); + assert_eq!(result2.output().unwrap(), &Bytes::new()); + + let target_acc = evm.ctx.journal_mut().state.get(&created_address).unwrap(); + assert_eq!(target_acc.info.code_hash, KECCAK_EMPTY); + assert_eq!( + target_acc.status, + AccountStatus::Touched | AccountStatus::LoadedAsNotExisting + ); +} + +/// Same scenario as above, but T1 and T2 run in separate finalize-per-tx +/// sessions (`ExecuteEvm::transact()`'s pattern) sharing only the underlying, +/// persistent `Database` - as opposed to one shared journal across both. +#[test] +fn test_call_after_create_destroy_standalone_journal() { + use revm::database::DatabaseCommit; + + let db = funded_cache_db(BENCH_CALLER); + + let mut evm1 = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::CANCUN) + .with_db(db) + .build_mainnet(); + + let result1 = evm1 + .transact_one( + TxEnv::builder_for_bench() + .kind(TxKind::Create) + .data(Bytes::copy_from_slice(SELFDESTRUCT_BYTECODE)) + .build_fill(), + ) + .unwrap(); + assert!(result1.is_success()); + let created_address = result1.created_address().unwrap(); + + let state1 = evm1.finalize(); + let mut db = evm1.ctx.journal_mut().database.clone(); + db.commit(state1); + + let mut evm2 = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::CANCUN) + .with_db(db) + .build_mainnet(); + + let result2 = evm2 + .transact_one( + TxEnv::builder_for_bench() + .nonce(1) + .kind(TxKind::Call(created_address)) + .data(Bytes::from_static(&[0xaa, 0xbb, 0xcc, 0xdd])) + .build_fill(), + ) + .unwrap(); + + assert!(result2.is_success()); + assert_eq!(result2.output().unwrap(), &Bytes::new()); + + let target_acc = evm2.ctx.journal_mut().state.get(&created_address).unwrap(); + assert_eq!(target_acc.info.code_hash, KECCAK_EMPTY); + assert_eq!( + target_acc.status, + AccountStatus::Touched | AccountStatus::LoadedAsNotExisting + ); +} + +const EXTCODESIZE_CALLER_BYTECODE: &[u8] = &[ + opcode::CALLER, + opcode::EXTCODESIZE, + opcode::POP, + opcode::STOP, +]; + +/// Regression test: per EIP-2929, a transaction's own sender is always pre-warmed by that +/// transaction's own validation, so `EXTCODESIZE(CALLER)` inside it must always cost the warm +/// price (100 gas) - regardless of whether an earlier, unrelated transaction already committed +/// to this shared journal (single-journal, multi-tx execution mode). +/// +/// T0 is a wholly unrelated transaction from `decoy`, committed to the shared journal first, so +/// that by the time T1 runs, this journal already has committed history - `caller` (T1's sender) +/// has never appeared in it before. If `caller`'s sender-warmth from T1's own validation doesn't +/// survive to the `EXTCODESIZE(CALLER)` a few instructions later, that opcode gets charged the +/// cold surcharge (2600 gas) instead. +/// +/// Root cause (fixed): `From for Account` (`crates/state/src/lib.rs`) hardcoded +/// `transaction_id: 0` for any account loaded fresh from the database, instead of the journal's +/// real current transaction id. That stamp only accidentally matched when the journal's very +/// first transaction had a real id of 0; once an earlier transaction had already committed and +/// advanced the id, the account's *second* touch within its own transaction (e.g. EXTCODESIZE +/// after validation's own load) was misread as a new transaction touching it for the first time. +#[test] +fn test_sender_extcodesize_stays_warm_after_prior_committed_tx() { + use revm::state::AccountInfo; + + let decoy = address!("0x2000000000000000000000000000000000000000"); + let caller = address!("0x1000000000000000000000000000000000000000"); + let probe = address!("0x4000000000000000000000000000000000000000"); + + let mut db = funded_cache_db(decoy); + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(10_000_000_000_000_000_000u128), + ..Default::default() + }, + ); + db.insert_account_info( + probe, + AccountInfo { + code_hash: Bytecode::new_raw(Bytes::from_static(EXTCODESIZE_CALLER_BYTECODE)) + .hash_slow(), + code: Some(Bytecode::new_raw(Bytes::from_static( + EXTCODESIZE_CALLER_BYTECODE, + ))), + ..Default::default() + }, + ); + + let mut evm = Context::mainnet() + .modify_cfg_chained(|cfg| cfg.spec = SpecId::CANCUN) + .with_db(db) + .build_mainnet(); + + let result0 = evm + .transact_one(TxEnv { + caller: decoy, + kind: TxKind::Call(decoy), + ..Default::default() + }) + .unwrap(); + assert!(result0.is_success()); + evm.ctx.journal_mut().commit_tx(); + + let result1 = evm + .transact_one(TxEnv { + caller, + kind: TxKind::Call(probe), + ..Default::default() + }) + .unwrap(); + evm.ctx.journal_mut().commit_tx(); + + assert!(result1.is_success()); + // 21000 (intrinsic) + 2 (CALLER) + 100 (EXTCODESIZE, warm) + 2 (POP) + 0 (STOP) = 21104. + // Comes out 23604 if EXTCODESIZE(CALLER) is incorrectly charged cold (2600) instead. + assert_eq!( + result1.gas_used(), + 21104, + "EXTCODESIZE(CALLER) must be charged the warm price (100 gas): `caller` is T1's own \ + sender and must be pre-warmed by T1's own validation, regardless of T0 having already \ + committed to this shared journal" + ); +} diff --git a/crates/ee-tests/tests/revm_testdata/test_multi_tx_create.json b/crates/ee-tests/tests/revm_testdata/test_multi_tx_create.json index 60cff1e58f..c3193b75db 100644 --- a/crates/ee-tests/tests/revm_testdata/test_multi_tx_create.json +++ b/crates/ee-tests/tests/revm_testdata/test_multi_tx_create.json @@ -112,7 +112,7 @@ "code_hash": "0x9125466aa9ef15459d85e7318f6d3bdc5f6978c0565bee37a8e768d7c202a67a", "nonce": 1 }, - "status": "Created | CreatedLocal | SelfDestructed | Touched | LoadedAsNotExisting", + "status": "Created | CreatedLocal | Touched | LoadedAsNotExisting", "storage": {}, "transaction_id": 2 }, diff --git a/crates/handler/src/frame.rs b/crates/handler/src/frame.rs index 823ec568df..9be7175ad6 100644 --- a/crates/handler/src/frame.rs +++ b/crates/handler/src/frame.rs @@ -3,6 +3,7 @@ use crate::item_or_result::FrameInitOrResult; use crate::{precompile_provider::PrecompileProvider, ItemOrResult}; use crate::{CallFrame, CreateFrame, FrameData, FrameResult}; use context::result::FromStringError; +use context::Transaction; use context_interface::context::ContextError; use context_interface::local::{FrameToken, OutFrame}; use context_interface::ContextTr; @@ -21,6 +22,7 @@ use interpreter::{ FrameInput, Gas, InputsImpl, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, SharedMemory, }; +use precompile::PrecompileId; use primitives::{ constants::CALL_STACK_LIMIT, hardfork::SpecId::{self, HOMESTEAD, LONDON, SPURIOUS_DRAGON}, @@ -169,7 +171,6 @@ impl EthFrame { // Create subroutine checkpoint let checkpoint = ctx.journal_mut().checkpoint(); - // Touch address. For "EIP-158 State Clear", this will erase empty accounts. if let CallValue::Transfer(value) = inputs.value { // Transfer value from caller to called account @@ -183,7 +184,7 @@ impl EthFrame { } } - let interpreter_input = InputsImpl { + let mut interpreter_input = InputsImpl { target_address: inputs.target_address, caller_address: inputs.caller, bytecode_address: Some(inputs.bytecode_address), @@ -193,6 +194,17 @@ impl EthFrame { let is_static = inputs.is_static; let gas_limit = inputs.gas_limit; + // If the call's bytecode address is the TxHash precompile address, + // fetch the transaction hash from Tx and pass the hash to the + // interpreter by overwriting the interpreter_input's input with the hash. + if let Some(tx_hash_addr) = PrecompileId::TxHash.mainnet_address() { + if inputs.bytecode_address == tx_hash_addr { + let tx_hash = ctx.tx().tx_hash(); + interpreter_input.input = + CallInput::Bytes(Bytes::copy_from_slice(tx_hash.as_ref())); + } + } + if let Some(result) = precompiles .run( ctx, @@ -267,6 +279,7 @@ impl EthFrame { inputs: Box, ) -> Result, ERROR> { let spec = context.cfg().spec().into(); + let return_error = |e| { Ok(ItemOrResult::Result(FrameResult::Create(CreateOutcome { result: InterpreterResult { @@ -278,6 +291,10 @@ impl EthFrame { }))) }; + if !context.cfg().execution_mode().supports_contract_creation() { + return return_error(InstructionResult::NotActivated); + } + // Check depth if depth > CALL_STACK_LIMIT as usize { return return_error(InstructionResult::CallTooDeep); @@ -296,9 +313,8 @@ impl EthFrame { if caller_info.balance < inputs.value { return return_error(InstructionResult::OutOfFunds); } - - // Increase nonce of caller and check if it overflows let old_nonce = caller_info.nonce; + // Increase nonce of caller and check if it overflows let Some(new_nonce) = old_nonce.checked_add(1) else { return return_error(InstructionResult::Return); }; diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index ad0c73ba96..8c33481246 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -12,7 +12,10 @@ use context_interface::{ }; use interpreter::interpreter_action::FrameInit; use interpreter::{Gas, InitialAndFloorGas, SharedMemory}; +use primitives::supra_constants::{is_supra_reserved, is_vm_signer}; use primitives::U256; +use std::format; +use std::string::String; /// Trait for errors that can occur during EVM execution. /// @@ -147,19 +150,24 @@ pub trait Handler { let init_and_floor_gas = self.validate(evm)?; let eip7702_refund = self.pre_execution(evm)? as i64; let mut exec_result = self.execution(evm, &init_and_floor_gas)?; - self.post_execution(evm, &mut exec_result, init_and_floor_gas, eip7702_refund)?; + if evm.ctx().cfg().execution_mode().charges_gas() { + self.post_execution(evm, &mut exec_result, init_and_floor_gas, eip7702_refund)?; + } // Prepare the output self.execution_result(evm, exec_result) } - /// Validates the execution environment and transaction parameters. + /// Validates the execution environment, transaction parameters and caller address. + /// + /// The transaction caller is verified to not be one of the SUPRA reserved addresses for user transactions. /// /// Calculates initial and floor gas requirements and verifies they are covered by the gas limit. /// /// Validation against state is done later in pre-execution phase in deduct_caller function. #[inline] fn validate(&self, evm: &mut Self::Evm) -> Result { + self.validate_caller(evm)?; self.validate_env(evm)?; self.validate_initial_tx_gas(evm) } @@ -242,6 +250,44 @@ pub trait Handler { validation::validate_env(evm.ctx()) } + /// Validates caller, to reject user transactions having caller address matching any of + /// the SUPRA reserved addresses. + /// SUPRA reserved addresses are used by the system to initiate and execute internal transactions. + /// To avoid/prevent any EOA address collision with the reserved addresses, and corrupting + /// internal system state the user transactions ever having sender address matching any SUPRA + /// reserved one are rejected. + #[inline] + fn validate_caller(&self, evm: &Self::Evm) -> Result<(), Self::Error> { + let ctx = evm.ctx_ref(); + let execution_mode = ctx.cfg().execution_mode(); + let caller = ctx.tx().caller(); + // Supra reserved address is allowed either in system execution mode or in genesis + if is_supra_reserved(&caller) + && !(execution_mode.is_system() || execution_mode.is_genesis()) + { + Err(Self::Error::from( + InvalidTransaction::UnsupportedTransactionSender { + sender: caller, + msg: format!( + "Invalid caller: supra reserved address. TxnHash {}", + ctx.tx().tx_hash() + ), + }, + )) + } else if !is_vm_signer(&caller) && execution_mode.is_system() { + Err(Self::Error::from( + InvalidTransaction::UnsupportedTransactionSender { + sender: caller, + msg: String::from( + "Invalid caller: Expected VM_SIGNER as caller for system transactions.", + ), + }, + )) + } else { + Ok(()) + } + } + /// Calculates initial gas costs based on transaction type and input data. /// /// Includes additional costs for access list and authorization list. @@ -418,8 +464,14 @@ pub trait Handler { /// Processes the final execution output. /// - /// This method, retrieves the final state from the journal, converts internal results to the external output format. - /// Internal state is cleared and EVM is prepared for the next transaction. + /// This method retrieves the final state from the journal, converts internal results to the + /// external output format, and prepares the EVM for the next transaction. + /// + /// For [`ExecutionMode::ReadOnly`] transactions the journal is always discarded + /// via [`JournalTr::discard_tx`] rather than committed, preventing any side effects + /// (including account warmth) from leaking into subsequent transaction execution. + /// If the journal contains state-mutating entries the method returns an error, because + /// predicates are expected to be pure view functions. #[inline] fn execution_result( &mut self, @@ -432,8 +484,32 @@ pub trait Handler { Ok(_) => (), } + // output() only calls take_logs() — it does not touch the journal entry list, + // so entries are still available for mutation detection below. let exec_result = post_execution::output(evm.ctx(), result); + if evm.ctx_ref().cfg().execution_mode().is_read_only() { + // Capture task identity before the mutable journal borrow. + let txn_nonce = evm.ctx_ref().tx().nonce(); + + // Inspect journal entries while they are still present — commit_tx() and + // discard_tx() both clear the list, so this must happen first. + let has_mutations = evm.ctx().journal_mut().has_state_mutations(); + + // Always discard: execution in read-only mode must never persist state changes or account + // warmth into the accumulated block state, regardless of mutation outcome. + evm.ctx().journal_mut().discard_tx(); + evm.ctx().local_mut().clear(); + evm.frame_stack().clear(); + + if has_mutations { + return Err(Self::Error::from_string(format!( + "Execution in ReadOnly mode attempted state mutation: transcation_nonce={txn_nonce}" + ))); + } + return Ok(exec_result); + } + // commit transaction evm.ctx().journal_mut().commit_tx(); evm.ctx().local_mut().clear(); diff --git a/crates/handler/src/pre_execution.rs b/crates/handler/src/pre_execution.rs index 0c9bf87b9e..0b3d99a0e0 100644 --- a/crates/handler/src/pre_execution.rs +++ b/crates/handler/src/pre_execution.rs @@ -114,11 +114,14 @@ pub fn validate_against_state_and_deduct_caller< >( context: &mut CTX, ) -> Result<(), ERROR> { + let should_update_nonce = context.cfg().execution_mode().updates_nonce(); + let charges_gas = context.cfg().execution_mode().charges_gas(); let basefee = context.block().basefee() as u128; let blob_price = context.block().blob_gasprice().unwrap_or_default(); let is_balance_check_disabled = context.cfg().is_balance_check_disabled(); let is_eip3607_disabled = context.cfg().is_eip3607_disabled(); - let is_nonce_check_disabled = context.cfg().is_nonce_check_disabled(); + // nonce check will not be done if it is disabled, or execution mode does not assume nonce-change. + let is_nonce_check_disabled = context.cfg().is_nonce_check_disabled() || !should_update_nonce; let (tx, journal) = context.tx_journal_mut(); @@ -132,7 +135,11 @@ pub fn validate_against_state_and_deduct_caller< is_nonce_check_disabled, )?; - let max_balance_spending = tx.max_balance_spending()?; + let max_balance_spending = if charges_gas { + tx.max_balance_spending()? + } else { + tx.value() + }; // Check if account has enough balance for `gas_limit * max_fee`` and value transfer. // Transfer will be done inside `*_inner` functions. @@ -144,9 +151,12 @@ pub fn validate_against_state_and_deduct_caller< .into()); } - let effective_balance_spending = tx - .effective_balance_spending(basefee, blob_price) - .expect("effective balance is always smaller than max balance so it can't overflow"); + let effective_balance_spending = if charges_gas { + tx.effective_balance_spending(basefee, blob_price) + .expect("effective balance is always smaller than max balance so it can't overflow") + } else { + tx.value() + }; // subtracting max balance spending with value that is going to be deducted later in the call. let gas_balance_spending = effective_balance_spending - tx.value(); @@ -162,17 +172,28 @@ pub fn validate_against_state_and_deduct_caller< } let old_balance = caller_account.info.balance; - // Touch account so we know it is changed. - caller_account.mark_touch(); + // Touch account so we know it is changed. Marking happens via the guarded `touch()` call + // inside `caller_accounting_journal_entry` below, not here — calling `mark_touch()` first + // would defeat that guard (see its doc comment) and break the sticky, block-scoped `Touched` + // invariant. caller_account.info.balance = new_balance; - // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`. - if tx.kind().is_call() { - // Nonce is already checked - caller_account.info.nonce = caller_account.info.nonce.saturating_add(1); + if should_update_nonce { + // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`. + if tx.kind().is_call() { + // Nonce is already checked + caller_account.info.nonce = caller_account.info.nonce.saturating_add(1); + } } - journal.caller_accounting_journal_entry(tx.caller(), old_balance, tx.kind().is_call()); + // bump_nonce must be gated by should_update_nonce — otherwise a NonceChange journal entry + // is pushed even when the nonce was never incremented (e.g. ReadOnly mode), + // which would cause false-positive mutation detection in execution_result. + journal.caller_accounting_journal_entry( + tx.caller(), + old_balance, + should_update_nonce && tx.kind().is_call(), + ); Ok(()) } @@ -195,62 +216,66 @@ pub fn apply_eip7702_auth_list< let mut refunded_accounts = 0; for authorization in tx.authorization_list() { - // 1. Verify the chain id is either 0 or the chain's current ID. - let auth_chain_id = authorization.chain_id(); - if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) { - continue; - } - - // 2. Verify the `nonce` is less than `2**64 - 1`. - if authorization.nonce() == u64::MAX { - continue; - } - - // recover authority and authorized addresses. - // 3. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]` - let Some(authority) = authorization.authority() else { - continue; - }; + let (address, bytecode, hash) = { + // 1. Verify the chain id is either 0 or the chain's current ID. + let auth_chain_id = authorization.chain_id(); + if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) { + continue; + } - // warm authority account and check nonce. - // 4. Add `authority` to `accessed_addresses` (as defined in [EIP-2929](./eip-2929.md).) - let mut authority_acc = journal.load_account_code(authority)?; + // 2. Verify the `nonce` is less than `2**64 - 1`. + if authorization.nonce() == u64::MAX { + continue; + } - // 5. Verify the code of `authority` is either empty or already delegated. - if let Some(bytecode) = &authority_acc.info.code { - // if it is not empty and it is not eip7702 - if !bytecode.is_empty() && !bytecode.is_eip7702() { + // recover authority and authorized addresses. + // 3. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]` + let Some(authority) = authorization.authority() else { continue; + }; + + // warm authority account and check nonce. + // 4. Add `authority` to `accessed_addresses` (as defined in [EIP-2929](./eip-2929.md).) + let mut authority_acc = journal.load_account_code(authority)?; + + // 5. Verify the code of `authority` is either empty or already delegated. + if let Some(bytecode) = &authority_acc.info.code { + // if it is not empty and it is not eip7702 + if !bytecode.is_empty() && !bytecode.is_eip7702() { + continue; + } } - } - // 6. Verify the nonce of `authority` is equal to `nonce`. In case `authority` does not exist in the trie, verify that `nonce` is equal to `0`. - if authorization.nonce() != authority_acc.info.nonce { - continue; - } + // 6. Verify the nonce of `authority` is equal to `nonce`. In case `authority` does not exist in the trie, verify that `nonce` is equal to `0`. + if authorization.nonce() != authority_acc.info.nonce { + continue; + } - // 7. Add `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` gas to the global refund counter if `authority` exists in the trie. - if !(authority_acc.is_empty() && authority_acc.is_loaded_as_not_existing_not_touched()) { - refunded_accounts += 1; - } + // 7. Add `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` gas to the global refund counter if `authority` exists in the trie. + if !(authority_acc.is_empty() && authority_acc.is_loaded_as_not_existing_not_touched()) + { + refunded_accounts += 1; + } - // 8. Set the code of `authority` to be `0xef0100 || address`. This is a delegation designation. - // * As a special case, if `address` is `0x0000000000000000000000000000000000000000` do not write the designation. - // Clear the accounts code and reset the account's code hash to the empty hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. - let address = authorization.address(); - let (bytecode, hash) = if address.is_zero() { - (Bytecode::default(), KECCAK_EMPTY) - } else { - let bytecode = Bytecode::new_eip7702(address); - let hash = bytecode.hash_slow(); - (bytecode, hash) + // 8. Set the code of `authority` to be `0xef0100 || address`. This is a delegation designation. + // * As a special case, if `address` is `0x0000000000000000000000000000000000000000` do not write the designation. + // Clear the accounts code and reset the account's code hash to the empty hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. + let address = authorization.address(); + let (bytecode, hash) = if address.is_zero() { + (Bytecode::default(), KECCAK_EMPTY) + } else { + let bytecode = Bytecode::new_eip7702(address); + let hash = bytecode.hash_slow(); + (bytecode, hash) + }; + + // 9. Increase the nonce of `authority` by one. + authority_acc.info.nonce = authority_acc.info.nonce.saturating_add(1); + (authority, bytecode, hash) }; - authority_acc.info.code_hash = hash; - authority_acc.info.code = Some(bytecode); - - // 9. Increase the nonce of `authority` by one. - authority_acc.info.nonce = authority_acc.info.nonce.saturating_add(1); - authority_acc.mark_touch(); + // Create an entry for authority account in journal to mark it touched, and with code updated + journal.nonce_bump_journal_entry(address); + journal.set_code_with_hash(address, bytecode, hash); } let refunded_gas = diff --git a/crates/inspector/src/handler.rs b/crates/inspector/src/handler.rs index aad34460cc..8a513740f0 100644 --- a/crates/inspector/src/handler.rs +++ b/crates/inspector/src/handler.rs @@ -1,5 +1,5 @@ use crate::{Inspector, InspectorEvmTr, JournalExt}; -use context::{result::ExecutionResult, ContextTr, JournalEntry, Transaction}; +use context::{result::ExecutionResult, Cfg, ContextTr, JournalEntry, Transaction}; use handler::{evm::FrameTr, EvmTr, FrameResult, Handler, ItemOrResult}; use interpreter::{ instructions::InstructionTable, @@ -58,7 +58,9 @@ where let init_and_floor_gas = self.validate(evm)?; let eip7702_refund = self.pre_execution(evm)? as i64; let mut frame_result = self.inspect_execution(evm, &init_and_floor_gas)?; - self.post_execution(evm, &mut frame_result, init_and_floor_gas, eip7702_refund)?; + if evm.ctx().cfg().execution_mode().charges_gas() { + self.post_execution(evm, &mut frame_result, init_and_floor_gas, eip7702_refund)?; + } self.execution_result(evm, frame_result) } diff --git a/crates/inspector/src/inspector_tests.rs b/crates/inspector/src/inspector_tests.rs index 5916a75c86..74e8261931 100644 --- a/crates/inspector/src/inspector_tests.rs +++ b/crates/inspector/src/inspector_tests.rs @@ -1,14 +1,14 @@ #[cfg(test)] mod tests { use crate::{InspectEvm, InspectSystemCallEvm, Inspector}; - use context::{Context, TxEnv}; - use database::{BenchmarkDB, BENCH_CALLER, BENCH_TARGET}; - use handler::{MainBuilder, MainContext}; + use context::{Context, ContextTr, ExecutionMode, TxEnv}; + use database::{BenchmarkDB, BENCH_CALLER, BENCH_CALLER_BALANCE, BENCH_TARGET}; + use handler::{ExecuteEvm, MainBuilder, MainContext}; use interpreter::{ interpreter_types::{Jumps, MemoryTr, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, }; - use primitives::{address, Address, Bytes, Log, TxKind, U256}; + use primitives::{address, hardfork::SpecId, Address, Bytes, Log, TxKind, U256}; use state::{bytecode::opcode, AccountInfo, Bytecode}; #[derive(Debug, Clone)] @@ -800,4 +800,79 @@ mod tests { assert!(evm.inspector.get_step_count() > 0); } + + #[test] + fn test_inspect_read_only_gas_used_matches_non_inspector_path() { + // Large non-zero-byte calldata pushes the EIP-7623 floor well above what this + // trivial STOP-only contract actually spends executing. `Handler::run` (the plain, + // non-inspector path) already gates `post_execution` — and with it, the floor + // enforcement in `eip7623_check_gas_floor` — behind `charges_gas()`, so it never + // applies the floor in ReadOnly mode. If `InspectorHandler` ever loses that same + // gate, `inspect_one_tx` would apply the floor while `transact_one` does not, + // making the two paths silently report different `gas_used()` for the identical + // transaction — exactly the divergence that matters for a caller (e.g. an + // automation predicate check) that trusts `gas_used()` from either path equally. + let calldata = Bytes::from(vec![0xffu8; 4096]); + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(1_000_000) + .data(calldata) + .build() + .unwrap(); + + let bytecode = Bytecode::new_legacy(Bytes::from(vec![opcode::STOP])); + + let mut plain_evm = Context::mainnet() + .modify_cfg_chained(|cfg| { + cfg.spec = SpecId::PRAGUE; + cfg.execution_mode = ExecutionMode::ReadOnly; + }) + .with_db(BenchmarkDB::new_bytecode(bytecode.clone())) + .build_mainnet(); + let plain_result = plain_evm.transact_one(tx.clone()).unwrap(); + assert!(plain_result.is_success()); + + let mut inspected_evm = Context::mainnet() + .modify_cfg_chained(|cfg| { + cfg.spec = SpecId::PRAGUE; + cfg.execution_mode = ExecutionMode::ReadOnly; + }) + .with_db(BenchmarkDB::new_bytecode(bytecode)) + .build_mainnet_with_inspector(TestInspector::new()); + let inspected_result = inspected_evm.inspect_one_tx(tx).unwrap(); + assert!(inspected_result.is_success()); + + assert_eq!(inspected_result.gas_used(), plain_result.gas_used()); + } + + #[test] + fn test_inspect_user_mode_still_applies_gas_accounting() { + let bytecode = Bytecode::new_legacy(Bytes::from(vec![opcode::STOP])); + let mut evm = Context::mainnet() + .with_db(BenchmarkDB::new_bytecode(bytecode)) + .build_mainnet_with_inspector(TestInspector::new()); + + let result = evm + .inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .gas_price(20) + .build() + .unwrap(), + ) + .unwrap(); + assert!(result.is_success()); + + // Default `User` mode charges gas, so `post_execution` must still run through the + // inspector path: the caller is charged for gas and reimbursed for the unused portion, + // leaving a strictly lower balance than `BenchmarkDB` seeded it with. + let caller_after = evm.ctx.journal_mut().state.get(&BENCH_CALLER).unwrap(); + assert_eq!( + caller_after.info.balance, + BENCH_CALLER_BALANCE - U256::from(21_000 * 20) + ); + } } diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index f70421fc72..36c4638781 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -201,8 +201,9 @@ where new_balance = new_balance.max(tx.value()); } - // Touch account so we know it is changed. - caller_account.mark_touch(); + // Touch account so we know it is changed. Marking happens via the guarded `touch()` + // call inside `caller_accounting_journal_entry` below, not here — calling `mark_touch()` + // first would defeat that guard and break the sticky, block-scoped `Touched` invariant. caller_account.info.balance = new_balance; // Bump the nonce for calls. Nonce for CREATE will be bumped in `handle_create`. @@ -442,8 +443,10 @@ where .info .balance .saturating_add(U256::from(mint.unwrap_or_default())); - acc.mark_touch(); + // Touch account so we know it is changed. Marking happens via the guarded `touch()` + // call inside `caller_accounting_journal_entry` below, not here — calling `mark_touch()` + // first would defeat that guard and break the sticky, block-scoped `Touched` invariant. // add journal entry for accounts evm.ctx() .journal_mut() diff --git a/crates/op-revm/src/transaction/abstraction.rs b/crates/op-revm/src/transaction/abstraction.rs index 2043f1ca0a..25123cd66d 100644 --- a/crates/op-revm/src/transaction/abstraction.rs +++ b/crates/op-revm/src/transaction/abstraction.rs @@ -187,6 +187,10 @@ impl Transaction for OpTransaction { fn authorization_list(&self) -> impl Iterator> { self.base.authorization_list() } + + fn tx_hash(&self) -> B256 { + self.base.tx_hash() + } } impl OpTxTr for OpTransaction { diff --git a/crates/precompile/src/id.rs b/crates/precompile/src/id.rs index 49c8259fff..7da3b322c6 100644 --- a/crates/precompile/src/id.rs +++ b/crates/precompile/src/id.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use primitives::{address, Address}; +use primitives::{address, supra_constants::TX_HASH_ADDRESS, Address}; use crate::{Precompile, PrecompileSpecId}; @@ -44,6 +44,8 @@ pub enum PrecompileId { Bls12MapFp2ToGp2, /// ECDSA signature verification over the secp256r1 elliptic curve (also known as P-256 or prime256v1). P256Verify, + /// Transaction hash precompile. + TxHash, /// Custom precompile identifier. Custom(Cow<'static, str>), } @@ -78,6 +80,7 @@ impl PrecompileId { Self::Bls12MapFpToGp1 => address!("0x0000000000000000000000000000000000000010"), Self::Bls12MapFp2ToGp2 => address!("0x0000000000000000000000000000000000000011"), Self::P256Verify => address!("0x0000000000000000000000000000000000000012"), + Self::TxHash => TX_HASH_ADDRESS, Self::Custom(_) => return None, }; Some(address) @@ -104,6 +107,7 @@ impl PrecompileId { Self::Bls12MapFpToGp1 => "BLS12_MAP_FP_TO_G1", Self::Bls12MapFp2ToGp2 => "BLS12_MAP_FP2_TO_G2", Self::P256Verify => "P256VERIFY", + Self::TxHash => "TX_HASH", Self::Custom(a) => a.as_ref(), } } @@ -173,6 +177,7 @@ impl PrecompileId { crate::secp256r1::P256VERIFY_OSAKA } } + Self::TxHash => crate::tx_hash::TX_HASH, Self::Custom(_) => return None, }; diff --git a/crates/precompile/src/lib.rs b/crates/precompile/src/lib.rs index 92c420054f..ab97629bc8 100644 --- a/crates/precompile/src/lib.rs +++ b/crates/precompile/src/lib.rs @@ -21,6 +21,7 @@ pub mod kzg_point_evaluation; pub mod modexp; pub mod secp256k1; pub mod secp256r1; +pub mod tx_hash; pub mod utilities; pub use id::PrecompileId; @@ -195,6 +196,7 @@ impl Precompiles { INSTANCE.get_or_init(|| { let mut precompiles = Self::cancun().clone(); precompiles.extend(bls12_381::precompiles()); + precompiles.extend([tx_hash::TX_HASH]); precompiles }) } @@ -399,6 +401,7 @@ pub enum PrecompileSpecId { /// * `BLS12_PAIRING_CHECK` at address 0x0f /// * `BLS12_MAP_FP_TO_G1` at address 0x10 /// * `BLS12_MAP_FP2_TO_G2` at address 0x11 + /// * `TX_HASH` at address 0x53555001 PRAGUE, /// Osaka spec added changes to modexp precompile: /// * [`EIP-7823: Set upper bounds for MODEXP`](https://eips.ethereum.org/EIPS/eip-7823). diff --git a/crates/precompile/src/tx_hash.rs b/crates/precompile/src/tx_hash.rs new file mode 100644 index 0000000000..2083faf52a --- /dev/null +++ b/crates/precompile/src/tx_hash.rs @@ -0,0 +1,9 @@ +//! TX_HASH precompile is added to return the transaction hash of the +//! transaction +use primitives::supra_constants::TX_HASH_ADDRESS; + +use crate::{identity::identity_run, Precompile, PrecompileId}; + +/// TX_HASH precompile +pub const TX_HASH: Precompile = + Precompile::new(PrecompileId::TxHash, TX_HASH_ADDRESS, identity_run); diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 8fb8bceeb2..fa711d736f 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -26,6 +26,7 @@ pub mod eip7907; pub mod eip7918; pub mod hardfork; mod once_lock; +pub mod supra_constants; pub use constants::*; pub use once_lock::OnceLock; diff --git a/crates/primitives/src/supra_constants.rs b/crates/primitives/src/supra_constants.rs new file mode 100644 index 0000000000..8d31e47c35 --- /dev/null +++ b/crates/primitives/src/supra_constants.rs @@ -0,0 +1,50 @@ +//! Constants defined by SUPRA to facilitate execution flow extensions. +use alloy_primitives::Address; + +/// Converts [`u64`] to [`Address`] type. +pub const fn u64_to_address(x: u64) -> Address { + let x = x.to_be_bytes(); + Address::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], + ]) +} + +/// Supra Reserved address for VM SIGNER, +pub const VM_SIGNER: Address = u64_to_address(0x5355_5000); + +/// Supra Reserved address Precompile address to retrieve transaction hash +pub const TX_HASH_ADDRESS: Address = u64_to_address(0x5355_5001); + +/// [0x5355_5000, 0x53555_50FF] addresses are reserved as SUPRA special addresses. +const SUPRA_RESERVED_ADDRESSES_PREFIX_UPPER_BOUND: usize = 19; + +/// Checks whether specified input address is one of the SUPRA reserved ones. +pub fn is_supra_reserved(address: &Address) -> bool { + VM_SIGNER[..SUPRA_RESERVED_ADDRESSES_PREFIX_UPPER_BOUND] + .eq(&address[..SUPRA_RESERVED_ADDRESSES_PREFIX_UPPER_BOUND]) +} + +/// Checks whether specified input address is SUPRA reserved VM_SIGNER +pub fn is_vm_signer(address: &Address) -> bool { + VM_SIGNER.eq(address) +} + +#[cfg(test)] +mod tests { + use crate::supra_constants::{is_supra_reserved, TX_HASH_ADDRESS, VM_SIGNER}; + + #[test] + fn check_reserved_addresses() { + let addr5 = super::u64_to_address(0x5355_5005); + let last_reserved = super::u64_to_address(0x5355_50ff); + let any_low_address = super::u64_to_address(0x5355_4fff); + let any_up_address = super::u64_to_address(0x5355_5100); + let any_address = super::u64_to_address(0x1_5355_5000); + assert!(is_supra_reserved(&addr5)); + assert!(is_supra_reserved(&VM_SIGNER)); + assert!(is_supra_reserved(&TX_HASH_ADDRESS)); + assert!(!is_supra_reserved(&any_address)); + assert!(!is_supra_reserved(&any_low_address)); + assert!(!is_supra_reserved(&any_up_address)); + } +} diff --git a/crates/supra-extension/Cargo.toml b/crates/supra-extension/Cargo.toml new file mode 100644 index 0000000000..df3c36733e --- /dev/null +++ b/crates/supra-extension/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "revm-supra-extension" +version = "0.1.0" +license.workspace = true +authors.workspace = true +categories.workspace = true +keywords.workspace = true +repository.workspace = true +documentation.workspace = true +homepage.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +alloy-sol-types = { workspace = true } +alloy-contract = { workspace = true } +alloy-consensus = { workspace = true } +alloy-eips = { workspace = true } +serde = { workspace = true } +alloy = { workspace = true } +derive_more = { workspace = true, features = ["full"] } +derive-getters = { workspace = true } +thiserror = { workspace = true } +primitives = { workspace = true , features = ["rand"]} +context = { workspace = true } +alloy-serde = { workspace = true, optional = true } +anyhow = { workspace = true } +serde_json = { workspace = true } +bincode = { workspace = true, features = ["serde"] } +once_cell = { workspace = true } +serde_with = { workspace = true, features = ["hex"] } +enum-kinds = "0.5.1" +# Optional dependencies activated by the `build-utils` feature. +# These are also present in [build-dependencies] for this crate's own build.rs; +# they are listed here so that other crates can use the build_utils module as a +# [build-dependencies] entry with `features = ["build-utils"]`. +foundry-config = { workspace = true, optional = true } +toml = { workspace = true, optional = true } +foundry-compilers = { workspace = true , optional = true} + +[lints] +workspace = true + + +[build-dependencies] +#forge = { workspace = true } +#clap = { workspace = true } +#shlex = { workspace = true } +foundry-compilers = { workspace = true } +foundry-config = { workspace = true } +anyhow = { workspace = true } +toml = { workspace = true } +serde = {workspace = true } +serde_json = { workspace = true } +bincode = { workspace = true , features = ["serde"]} +enum-kinds = { workspace = true } + +[features] +serde = ["alloy-serde"] +# Exposes `revm_supra_extension::build_utils` — a set of utilities for compiling +# Solidity contracts and loading bytecodes inside `build.rs` scripts. +# Add this crate as a `[build-dependencies]` entry with `features = ["build-utils"]` +# to use them from another crate's build script. +build-utils = ["dep:foundry-config", "dep:toml", "dep:foundry-compilers"] diff --git a/crates/supra-extension/build.rs b/crates/supra-extension/build.rs new file mode 100644 index 0000000000..9adddb57d1 --- /dev/null +++ b/crates/supra-extension/build.rs @@ -0,0 +1,75 @@ +//! Prepares supra-extension by compiling smart-contracts and building rust bindings + +// Utility functions shared with the `build-utils` library feature — see src/build_utils.rs. +// Using include! keeps a single source of truth for CompileConfig, compile_contracts, +// load_contracts_bytecode, and dump_bytecodes without introducing a circular dependency +// between the build script and the library crate. +include!("build_utils_impl.rs"); + +const CURRENT_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +fn rebuild_rust_bindings() { + // 1. Tell Cargo to rerun the script if the contracts directory changes + let cargo_dir = PathBuf::from(CURRENT_DIR); + println!( + "cargo:rerun-if-changed={}/../../solidity/supra_contracts/src/SupraContractsBindings.sol", + cargo_dir.display() + ); + + // smr-moonshot referencing this version of the REVM has dependency conflicts caused by syn library used by forge. + // So unless the issue is fixed, rust bindings on updates of the SupraContractsBindings.sol will be regenerated manually + // and committed. This code should remain commented out otherwise + // To do it: + // - uncomment below code + // - uncomment build dependencies in Cargo.toml file of this project + // - uncomment forge library reference in top level Cargo.toml file + // - build the project + + // // Determine the output directory for the generated bindings + // use clap::Parser; + // use forge::cmd::bind::BindArgs; + // let contracts_relative_path = PathBuf::from("../../solidity/supra_contracts"); + // let contracts_build_config = + // cargo_dir.join(contracts_relative_path.join(PathBuf::from("foundry.toml"))); + // let contract_names = "SupraContracts"; + // + // let bindings_path = cargo_dir + // .join(PathBuf::from("src")) + // .join(PathBuf::from("supra_contract_bindings")); + // + // // Ensure the output directory exists + // std::fs::create_dir_all(bindings_path.as_path()).expect("Failed to create bindings directory"); + // let command_inputs = format!( + // "bind --bindings-path {} --overwrite --module --select {} --alloy --config-path {}", + // bindings_path.display(), + // contract_names, + // contracts_build_config.display() + // ); + // let parsed_inputs = shlex::split(&command_inputs).expect("Failed to parse command string"); + // let bind_cmd: BindArgs = + // BindArgs::try_parse_from(parsed_inputs).expect("Failed to parse command arguments"); + // bind_cmd.run().expect("Failed to execute bind command"); +} + +fn main() { + rebuild_rust_bindings(); + + let manifest_dir = Path::new(CURRENT_DIR); + let config = CompileConfig::load(manifest_dir).expect("Config should always be valid"); + // Rerun this script if the compile configuration changes (e.g. a contract name is added). + println!("cargo:rerun-if-changed={}/compile_config.toml", CURRENT_DIR); + + let supra_contracts_artifacts = compile_contracts(&config.contracts_dapp_path(manifest_dir)) + .expect("Successful supra contracts compilation"); + + let mut contracts_bytecode = BTreeMap::new(); + load_contracts_bytecode( + config.contract_names(), + &supra_contracts_artifacts, + &mut contracts_bytecode, + ) + .expect("Supra contracts loaded successfully"); + + dump_bytecodes(contracts_bytecode, "supra_contracts_bytecode") + .expect("Bytecodes dumped successfully"); +} diff --git a/crates/supra-extension/build_utils_impl.rs b/crates/supra-extension/build_utils_impl.rs new file mode 100644 index 0000000000..6b6e3aa08b --- /dev/null +++ b/crates/supra-extension/build_utils_impl.rs @@ -0,0 +1,146 @@ +use anyhow::Result; +use foundry_compilers::utils; +use foundry_config::Config; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +// Shared build-time utility implementations. +// +// This file is `include!`-d into both `build.rs` (build script) and +// `src/build_utils.rs` (library, behind the `build-utils` feature) so that +// the logic lives in exactly one place while being reachable from both contexts. + +/// Configuration for which Solidity contracts to compile, loaded from `compile_config.toml`. +#[derive(Serialize, Deserialize, Debug)] +pub struct CompileConfig { + /// Path to the foundry project, relative to the crate's manifest directory. + dapp_path: PathBuf, + /// Names of contracts whose bytecode should be extracted after compilation. + contract_names: Vec, +} + +impl CompileConfig { + /// Load configuration from `compile_config.toml` located in `manifest_dir`. + /// + /// `manifest_dir` is typically `env!("CARGO_MANIFEST_DIR")` when called from a + /// build script, and can be sourced from any directory when called from tests or + /// other tooling. + pub fn load(manifest_dir: &Path) -> Result { + let path = manifest_dir.join("compile_config.toml"); + toml::from_str::(&std::fs::read_to_string(path)?) + .map_err(|e| e.into()) + .inspect_err(|e| println!("Error: {}", e)) + } + + /// Resolve and canonicalize the foundry project directory relative to `manifest_dir`. + pub fn contracts_dapp_path(&self, manifest_dir: &Path) -> PathBuf { + utils::canonicalize(manifest_dir.join(&self.dapp_path)) + .expect("failed to canonicalize dapp path") + } + + /// Names of contracts to compile and embed. + pub fn contract_names(&self) -> &[String] { + &self.contract_names + } +} + +/// Compile all Solidity contracts in the foundry project rooted at `path`. +/// +/// Emits `cargo:rerun-if-changed` for every Solidity source file discovered by +/// the project, and sets `cargo:rustc-env=COMPILED_CONTRACTS_DIR` to the +/// artifacts directory so downstream code can locate the compiled output. +/// +/// Returns the path to the compiled artifacts directory. +pub fn compile_contracts(path: &impl AsRef) -> Result { + let foundry_config = Config::load_with_root(path.as_ref())?.sanitized(); + let _ = foundry_config.install_lib_dir(); + let project = foundry_config.project()?; + + let output = project.compile()?; + let _ = output.succeeded(); + // Instruct Cargo to re-run this build script whenever a Solidity source changes. + project.rerun_if_sources_changed(); + + let artifacts_dir = project.paths.artifacts.clone(); + println!( + "cargo:rustc-env=COMPILED_CONTRACTS_DIR={}", + artifacts_dir.display() + ); + + Ok(artifacts_dir) +} + +/// Populate `bytecodes` with the compiled deployment bytecode for each contract in +/// `contract_names`, reading Foundry's default artifact layout under `artifacts_path`: +/// `/.sol/.json`. +/// +/// Returns an error if any artifact is missing, its bytecode field is empty, or a +/// contract name appears more than once. +pub fn load_contracts_bytecode( + contract_names: &[String], + artifacts_path: &Path, + bytecodes: &mut BTreeMap>, +) -> Result<()> { + for contract_name in contract_names { + let path = artifacts_path + .join(format!("{contract_name}.sol")) + .join(format!("{contract_name}.json")); + + if !path.exists() { + return Err(anyhow::anyhow!( + "Failed to find contract artifact at: {}", + path.display() + )); + } + + let file = std::fs::File::open(&path)?; + let buf_reader = std::io::BufReader::new(file); + let contract: foundry_compilers::artifacts::ContractBytecode = + serde_json::from_reader(buf_reader)?; + + let bytecode: Vec = contract + .bytecode + .and_then(|b| b.bytes().cloned()) + .map(|b| b.to_vec()) + .filter(|b| !b.is_empty()) + .ok_or_else(|| { + anyhow::anyhow!("Failed to load bytecode for contract: {contract_name}") + })?; + + let inserted = bytecodes.insert(contract_name.to_string(), bytecode); + // Two contracts with the same name in the same artifacts tree is a configuration + // error — the second would silently overwrite the first if we allowed it. + if inserted.is_some() { + return Err(anyhow::anyhow!( + "Duplicate contract name: {contract_name} in {artifacts_path:?} path" + )); + } + } + Ok(()) +} + +/// Serialize `bytecodes` with bincode and write the result to +/// `$OUT_DIR/.bin`. +/// +/// Also emits `cargo:rustc-env=CONTRACTS_DUMPED=1` so that the library crate can +/// assert at compile time that the build step ran successfully. +/// +/// `OUT_DIR` is set by Cargo when executing build scripts; calling this function +/// outside a build script context will return an error. +pub fn dump_bytecodes(bytecodes: BTreeMap>, bin_file_name: &str) -> Result<()> { + let out_dir = std::env::var("OUT_DIR")?; + let out_path = Path::new(&out_dir) + .join(bin_file_name) + .with_extension("bin"); + + std::fs::write( + &out_path, + bincode::serde::encode_to_vec(&bytecodes, bincode::config::standard()) + .expect("Successful serialization"), + ) + .expect("Failed to write bytecodes to file"); + + println!("cargo:rustc-env=CONTRACTS_DUMPED=1"); + Ok(()) +} \ No newline at end of file diff --git a/crates/supra-extension/compile_config.toml b/crates/supra-extension/compile_config.toml new file mode 100644 index 0000000000..0e5298fd72 --- /dev/null +++ b/crates/supra-extension/compile_config.toml @@ -0,0 +1,20 @@ +dapp_path = "../../solidity/supra_contracts/" + +contract_names = [ + "MultiSignatureWallet", + "MultisigBeacon", + "BeaconProxy", + "ERC20Supra", + "ERC20SupraHandler", + "BlockMeta", + "ERC1967Proxy", + "DiamondCutFacet", + "Diamond", + "DiamondLoupeFacet", + "OwnershipFacet", + "ConfigFacet", + "RegistryFacet", + "CoreFacet", + "DiamondInit", +] + diff --git a/crates/supra-extension/src/AUTOMATION_REGISTRY_GAS_GUIDE.md b/crates/supra-extension/src/AUTOMATION_REGISTRY_GAS_GUIDE.md new file mode 100644 index 0000000000..43d942d5b6 --- /dev/null +++ b/crates/supra-extension/src/AUTOMATION_REGISTRY_GAS_GUIDE.md @@ -0,0 +1,301 @@ +# Automation Registry Cycle-Transition Gas Guide + +This is a downstream-facing reference for sizing the `gas_limit` of the bookkeeping +transactions (`AutomationRegistryRecord`, see `transactions/automation_record.rs`) that +drive an automation-registry cycle transition: the transaction that triggers the +transition (`monitorCycleEnd` or `disableAutomation`) and the `processTasks` +transactions that carry it to completion. + +`contracts/configs.rs`'s `MAX_SUPPORTED_AUTOMATION_TASKS` doc comment already covers +`monitorCycleEnd`'s cost in isolation. This guide adds the rest of the picture: what +`processTasks` costs per batch, and how that differs across the three ways a +transition can happen. All figures come from +`solidity/supra_contracts/test/CycleTransitionGas.t.sol` — reproduce with: + +``` +cd solidity/supra_contracts && forge test --match-contract CycleTransitionGasTest -vv +``` + +**Assumptions common to all three scenarios below**, matching production defaults +(`LibDiamondUtils.defaultInitParams()`): +- Registry at its production cap: 200 tasks (160 UST + 40 GST), matching + `MAX_SUPPORTED_AUTOMATION_TASKS`. +- `processTasks` submitted in batches of 25 tasks (8 batches to cover the full + registry). This is the *submitter's* convention, not an on-chain constant — no + batch-size cap exists in the contract or in this crate. +- Figures are `gasleft()`-bracket measurements around a single external call, the + same methodology `MonitorCycleEndGas.t.sol` uses (not `forge snapshot`/gas-report + tooling, which this repo doesn't use). They reflect gas charged at each call site + and are best read as *relative* figures for sizing purposes, not a promise of the + exact number a live network will report — reproduce and re-check after any change + to the registry's storage layout or transition logic. + +## Running with a custom task count + +The task counts are overridable via environment variables, so this benchmark can be +re-run for a hypothetical registry size without editing the test file. All three env +vars fall back to the production defaults (160 / 40 / 20) when unset, so a plain +`forge test` run is unaffected: + +| Env var | Default | Applies to | +| --- | --- | --- | +| `CYCLE_GAS_BENCH_UST_COUNT` | 160 | all three scenarios | +| `CYCLE_GAS_BENCH_GST_COUNT` | 40 | all three scenarios | +| `CYCLE_GAS_BENCH_EXPIRING_UST_COUNT` | 20 | scenario 3 only (must be ≤ the UST count) | + +Example — re-run all three scenarios at a smaller, non-default size (73 tasks: 60 +UST + 13 GST, 5 of them expiring in scenario 3): + +``` +CYCLE_GAS_BENCH_UST_COUNT=60 \ +CYCLE_GAS_BENCH_GST_COUNT=13 \ +CYCLE_GAS_BENCH_EXPIRING_UST_COUNT=5 \ +forge test --match-contract CycleTransitionGasTest -vv +``` + +Counts that exceed the production capacity (160 UST / 40 GST) are handled too — the +test widens `taskCapacity`/`sysTaskCapacity` (and their paired gas caps) just enough +to fit, so you can probe beyond the current production cap, e.g. to see where the +finalization premium heads at 310 tasks: + +``` +CYCLE_GAS_BENCH_UST_COUNT=250 CYCLE_GAS_BENCH_GST_COUNT=60 \ +forge test --match-test testCycleTransitionGas_FullFlow_ProductionMix -vv +``` + +The task count need not be a multiple of the 25-task batch size — the last batch is +simply smaller. The `SANITY_GAS_CEILING` (30M) assertion in each test is a fixed, +generic ceiling, not scaled to the custom count — a large enough custom N can +legitimately trip it; that's expected, not a contract bug, and the logged per-batch +figures are the real data to read in that case. + +## How `monitorCycleEnd` relates to `BlockMeta::blockPrologue` + +`monitorCycleEnd` doesn't run as a standalone transaction. It runs because it's +**registered as one entry in a separate system contract, `BlockMeta`** +(`solidity/supra_contracts/src/BlockMeta.sol`), which the Supra VM calls once per +block, at block-start, via `blockPrologue()`. `BlockMeta` maintains an ordered list +of `(target contract, selector, gasLimit)` entries — a "cron-within-a-block" for +system hooks that must run every block without a user transaction — and +`blockPrologue()` just loops that list: + +``` +// BlockMeta.sol +function blockPrologue() external { + msg.sender.enforceIsVmSigner(); + for (uint256 i = 0; i < executions.length; i++) { + uint256 entry = executions[i]; + uint64 gasLimit = uint64(entry); // this entry's OWN registered gas limit + (address target, bytes4 selector) = unpackExecution(entry); + (bool ok, bytes memory data) = target.call{gas: gasLimit}(abi.encodePacked(selector)); + if (ok) { emit CallSucceeded(target, selector); } else { emit CallFailed(target, selector, data); } + } +} +``` + +`monitorCycleEnd` was added to that list via a governance/multisig action +(`InitializeCycleMonitoring` in `solidity/supra_contracts/script/GovActions.s.sol`, +submitted through `run_steps.sh`), which calls +`BlockMeta.register(registry, monitorCycleEnd.selector, selectorGasLimit)`. +**`selectorGasLimit` is an operator-chosen value, not hardcoded anywhere in this +repo** — whoever submits that governance action sets it via the `SELECTOR_GAS_LIMIT` +env var. This is the actual `monitorCycleEnd` gas budget the guidance in this +document is sizing against. + +### Two gas ceilings, only one of which is enforced in code + +There are two distinct "total gas" concepts here, and only a genesis-time check +connects them: + +1. **`BlockMeta.blockPrologueGasCap`** — an on-chain `uint64` state variable, the sum + that all registered entries' individual gas limits must fit under. Enforced at + *registration time only*, in `register()`: + ``` + uint256 upperBound = forwardingRuleCompatibleUpperBoundGasCap(blockPrologueGasCap); // cap * 63/64 + require(uint256(totalGasAllocated) + _gasLimit <= upperBound, GasCapExceeded()); + ``` + The `* 63/64` factor accounts for the EVM's own forwarding rule (a `call` can + never forward more than 63/64 of the gas available to its caller), so the sum of + what `blockPrologue()` hands out to entries has to leave that margin. The owner + can raise this cap later via `setBlockPrologueGasCap`. +2. **`DEFAULT_BLOCK_METADATA_GAS_LIMIT`** (`crates/supra-extension/src/transactions/block_metadata.rs`, + = `TX_GAS_LIMIT_CAP` = 16,777,216, the EIP-7825 network-wide transaction gas cap) + — the ceiling on the *outer* `BlockMetadata` system transaction that carries the + `blockPrologue()` call itself. This is a Rust/consensus-layer constant; Solidity + has no reference to it at all. + +The **only** code-level tie between the two is at genesis: +`GenesisTransactionGeneratorConfig::is_valid()` (`crates/supra-extension/src/contracts/configs.rs`) +rejects a genesis `block_prologue_gas_cap` greater than `DEFAULT_BLOCK_METADATA_GAS_LIMIT`. +**After genesis, nothing stops `blockPrologueGasCap` from being raised (via +`setBlockPrologueGasCap`) past what the outer transaction can actually ever afford.** +If that happens, `register()` will happily accept more/bigger entries than the +transaction can really fund, and at execution time individual `call{gas: gasLimit}` +sub-calls will just silently receive less gas than their registered limit once the +outer call frame runs low — showing up as spurious `CallFailed` events, not a +revert with a clear reason. + +### Why a mis-sized `monitorCycleEnd` entry fails quietly, not loudly + +`blockPrologue()`'s loop does not `require(ok)` — a failing entry only emits +`CallFailed` and the loop moves on to the next entry. So if `monitorCycleEnd`'s +*actual* cost (driven by `taskCapacity + sysTaskCapacity`, per Scenario 1 above) +exceeds its *registered* `selectorGasLimit`, the symptom is not a reverted block or +a loud error: **the automation cycle simply stops advancing**, silently, block after +block, observable only via `CallFailed` events on `BlockMeta` — every other +registered entry keeps running fine. + +### The capacity/gas-limit gap this repo cannot close for you + +`ConfigFacet.updateConfigBuffer`'s own NatSpec says it plainly (`ConfigFacet.sol`): + +> `_taskCapacity` and `_sysTaskCapacity` bound how many tasks `CoreFacet.monitorCycleEnd()` +> iterates over each cycle, so raising them raises that function's per-call gas cost. +> ... This contract has no on-chain reference to `BlockMeta` and intentionally does +> not cap `_taskCapacity`/`_sysTaskCapacity` against it ... increasing these values is +> NOT automatically safe. + +Confirmed by reading `LibCommon.validateConfigParameters` directly: it checks +non-zero values and simple orderings, and never references `BlockMeta`, +`blockPrologueGasCap`, or any gas-limit constant. **There is no code path anywhere +in this repo that checks the automation registry's task capacity against +`monitorCycleEnd`'s registered `BlockMeta` gas limit, in either direction.** The two +are governed independently: + +| Who controls it | What | Where | +| --- | --- | --- | +| `BlockMeta`'s owner (foundation multisig) | `monitorCycleEnd`'s registered `selectorGasLimit`, `blockPrologueGasCap` | `BlockMeta.register` / `.setBlockPrologueGasCap`, via `InitializeCycleMonitoring`/governance actions | +| The automation registry's owner/governance | `taskCapacity`, `sysTaskCapacity` | `ConfigFacet.updateConfigBuffer` | + +**Guideline: governance must treat these as one joint change, never independently.** +Before raising `taskCapacity`/`sysTaskCapacity` beyond the current production +default (160/40 = 200, `MAX_SUPPORTED_AUTOMATION_TASKS`), re-run this benchmark +(`Running with a custom task count` above) at the target N, and confirm the +resulting `monitorCycleEnd` gas figure still fits under `monitorCycleEnd`'s +currently-registered `BlockMeta` gas limit — re-registering it with a higher +`selectorGasLimit` first if it doesn't, and confirming that fits under +`blockPrologueGasCap` (with headroom left for whatever other entries are also +registered there) and, transitively, under `DEFAULT_BLOCK_METADATA_GAS_LIMIT`. +Doing it in the other order — raising capacity first — will silently stall +automation the moment `monitorCycleEnd`'s real cost outgrows its registered limit. + +## Scenario 1 — normal transition, everything survives (`FINISHED -> STARTED`) + +The common case: automation stays enabled, no task expires mid-transition. + +| Call | Gas | +| --- | --- | +| `monitorCycleEnd` (trigger) | 4,683,155 | +| `processTasks`, non-final batch (typical, batches 1–7) | ~992,652 | +| `processTasks`, **final batch (8/8)** | **5,380,804** | +| Final-batch finalization premium (final − typical) | ~4,388,152 | +| Total `processTasks` (8 batches) | 12,329,372 | +| **Grand total** (trigger + all batches) | **17,012,527** | + +The final batch of a `FINISHED->STARTED` transition is ~5.4x a typical batch. That +premium comes from three things landing on whichever call happens to finalize the +transition (`LibCore.sol`): +1. `updateRegistryState`'s two O(n) array writes — `registryState.activeTaskIds` and + `registryState.orderedTaskIds` are both freshly assigned the full survivor list + (fresh nonzero SSTOREs, not cheap storage-clear refunds). +2. `moveToStartedState`'s `delete` of the whole transition-state struct (clears + `expectedTasksToBeProcessed` and `survivedTaskIds`, up to 200 elements each). +3. The batch's own per-task `survivedTaskIds.push()` cost, same as any other batch. + +**This is the worst case among the three scenarios** — see Scenario 2 below, where +the equivalent finalization is actually free. + +## Scenario 2 — mid-cycle suspension, everything dropped (`STARTED -> SUSPENDED -> READY`) + +Automation is disabled mid-cycle (`disableAutomation`, contract-owner-only), well +before the cycle would otherwise end. Every registered task is refunded and removed; +none survive into a next cycle, and the cycle index does not increment. + +| Call | Gas | +| --- | --- | +| `disableAutomation` (trigger) | 4,684,231 | +| `processTasks` (`onCycleSuspend`), non-final batch (typical) | ~573,331 | +| `processTasks`, **final batch (8/8)** | **435,486** | +| Final-batch finalization premium | **0** (final batch is *cheaper* than typical) | +| Total `processTasks` (8 batches) | 4,448,803 | +| **Grand total** (trigger + all batches) | **9,133,034** | + +Two things stand out relative to Scenario 1: +- **No survivor bookkeeping**: `onCycleSuspend` never pushes to `survivedTaskIds` — + every task is unconditionally removed and refunded, so there's no per-survivor + push cost building up across batches. +- **Finalization is a clear, not a write**: on the `SUSPENDED` branch, + `updateRegistryState` resets `activeTaskIds`/`orderedTaskIds` to empty arrays and + clears `sysTaskIds` — storage-clearing operations (partially refunded gas under + EIP-3529), not fresh nonzero writes. The finalizing batch here is *cheaper* than a + typical batch, the opposite of Scenario 1. **Do not size a suspension-path + `processTasks` record off Scenario 1's final-batch premium** — it doesn't apply + here. + +## Scenario 3 — normal transition with some tasks expiring (`FINISHED -> STARTED`, cycle 2) + +Same as Scenario 1, but 20 of the 160 UST tasks are past their expiry by the time +this transition processes them, so they get refunded and dropped +(`refundDepositAndDrop`) instead of renewed (`survivedTaskIds.push`). Because +registration always requires `expiry > current cycle's end time` +(`LibRegistry.validateTaskDuration`), a task can never already be expired at the +very first transition it survives into — this scenario necessarily spans two +cycles: the 20 tasks are registered with an expiry inside cycle 2, survive cycle +1's transition, and are dropped at cycle 2's. + +| Call | Gas | +| --- | --- | +| `monitorCycleEnd` (trigger, cycle 2) | 4,250,955 | +| `processTasks`, non-final batch (typical) | ~914,363 | +| `processTasks`, **final batch (8/8)** | **933,460** | +| Final-batch finalization premium | ~19,097 | +| Total `processTasks` (8 batches) | 7,334,003 | +| **Grand total** (trigger + all batches) | **11,584,958** | + +With 180 survivors instead of 200, the finalization premium collapses to ~19k gas — +consistent with Scenario 1's premium being proportional to *survivor* count +(`updateRegistryState`'s array writes), not total task count: fewer survivors means +smaller arrays to write at finalization. The batches containing the 20 expiring +tasks (batch 1, dominated by expired-task drops) are cheaper than a normal batch, +since a refund-and-drop is less work than a full fee-charge-and-survive path. + +## Guidance for downstream `gas_limit` sizing + +### Where things stand today: a flat cap, not per-record sizing + +Every `processTasks` `AutomationRegistryRecord` is currently assigned the same flat +`gas_limit` — `TX_GAS_LIMIT_CAP` (16,777,216, `crates/primitives/src/eip7825.rs`), +per the node's transaction-construction code (`evm/records.rs`, outside this repo). +There is no per-batch variable budget today, and no use of +`ICoreFacet.getCycleStateDetails()` to detect and specially size a transition's +final batch. + +Measured against that flat cap, the worst case across all three scenarios is +**Scenario 1's final batch at 5,380,804 gas** — about **3.1x headroom** +(16,777,216 / 5,380,804) under the current 16,777,216 flat limit. **Given that +margin, the "an under-budget final batch cannot be fixed by splitting it further +after the fact" hazard is not live today.** This section exists so that headroom +has a documented, reproducible baseline: if `TX_GAS_LIMIT_CAP` is ever lowered, or +`processTasks` sizing ever moves to a variable per-record budget (using +`getCycleStateDetails()`'s `nextTaskIndexPosition` vs +`expectedTasksToBeProcessed.length` to detect the final batch, as one could +imagine doing), re-run this benchmark and re-check the margin against whatever the +new scheme assigns non-final vs. final batches. + +- **Trigger call** (`monitorCycleEnd` / `disableAutomation`): size to at least + ~4.7M gas at the 200-task cap, consistent with `configs.rs`'s existing + `MAX_SUPPORTED_AUTOMATION_TASKS` justification. For `monitorCycleEnd`, "size" + means the `selectorGasLimit` it was registered with in `BlockMeta` (see "How + `monitorCycleEnd` relates to `BlockMeta::blockPrologue`" above) — this is **not** + automatically kept in sync with `taskCapacity`/`sysTaskCapacity`, so re-check it + specifically whenever either capacity changes. `disableAutomation` is a regular + transaction and needs its own explicit budget of similar size. +- **`processTasks`**: comfortably covered by the current flat 16,777,216 cap at + every batch size measured here (typical batches ~993k/~573k/~914k gas; the + worst-case final batch at 5,380,804 gas) — see the margin above. If a future + change introduces a smaller or variable per-record budget instead of the flat + cap, use **~5.4M gas** (Scenario 1's measured worst case) as the floor for + whichever batch will finalize a `FINISHED->STARTED` transition, and ~1M gas for + every other batch, including suspension-path finalization (Scenario 2's final + batch is cheaper than typical, not more expensive — see Scenario 2 above for why + that doesn't generalize to the `FINISHED->STARTED` case). diff --git a/crates/supra-extension/src/build_utils.rs b/crates/supra-extension/src/build_utils.rs new file mode 100644 index 0000000000..1a496249f9 --- /dev/null +++ b/crates/supra-extension/src/build_utils.rs @@ -0,0 +1,34 @@ +//! Build-time utilities for Solidity contract compilation and bytecode extraction. +//! +//! These utilities are designed for use inside `build.rs` scripts. Other crates can +//! consume them by adding `revm-supra-extension` as a `[build-dependencies]` entry +//! with `features = ["build-utils"]`: +//! +//! ```toml +//! [build-dependencies] +//! revm-supra-extension = { ..., features = ["build-utils"] } +//! ``` +//! +//! Then in the consuming `build.rs`: +//! +//! ```rust,ignore +//! use revm_supra_extension::build_utils::{CompileConfig, compile_contracts, +//! load_contracts_bytecode, dump_bytecodes}; +//! use std::collections::BTreeMap; +//! use std::path::Path; +//! +//! fn main() { +//! let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); +//! let config = CompileConfig::load(manifest_dir).unwrap(); +//! let artifacts = compile_contracts(&config.contracts_dapp_path(manifest_dir)).unwrap(); +//! let mut bytecodes = BTreeMap::new(); +//! load_contracts_bytecode(config.contract_names(), &artifacts, &mut bytecodes).unwrap(); +//! dump_bytecodes(bytecodes, "my_contracts_bytecode").unwrap(); +//! } +//! ``` + +// Shared implementation with build.rs — see build_utils_impl.rs at the crate root. +// Both build.rs (build script) and this module include the same file to avoid +// duplicating ~120 lines of implementation while staying within Rust's constraint +// that a build script cannot depend on its own library crate. +include!("../build_utils_impl.rs"); diff --git a/crates/supra-extension/src/contracts/canonical_singletons.rs b/crates/supra-extension/src/contracts/canonical_singletons.rs new file mode 100644 index 0000000000..5e2bc0dae2 --- /dev/null +++ b/crates/supra-extension/src/contracts/canonical_singletons.rs @@ -0,0 +1,284 @@ +//! Canonical, address-fixed EVM singleton contracts that the wider ecosystem (Foundry, +//! hardhat-deploy, ERC-777, account-abstraction tooling, etc.) expects to find at specific +//! well-known addresses on any EVM chain, alongside the genesis transactions that deploy them. +//! +//! Each contract here is deployed via a plain `CREATE` from its own fixed, independent +//! deployer account at nonce 0, reproducing the exact address the contract already holds +//! on other EVM chains. The init-code is embedded as a static binary asset +//! (`canonical_singletons_bytecode/*.bin`) extracted verbatim from each project's +//! canonical historical deployment transaction — not compiled from vendored Solidity source, +//! since these are third-party contracts we don't own or modify, and any difference in +//! compiler/optimizer/metadata would risk producing bytecode that isn't byte-identical to +//! what's deployed everywhere else. + +use crate::contracts::transaction::GenesisTransaction; +use primitives::{address, Address, TxKind}; + +/// The address that deploys the default CREATE2 deployer contract. +pub const CREATE2_FACTORY_OWNER: Address = address!("0x3fAB184622Dc19b6109349B94811493BF2a45362"); + +/// The default CREATE2 FACTORY contract address. Assumed deployed by [CREATE2_FACTORY_OWNER] with nonce 0 +pub const CREATE2_FACTORY_ADDRESS: Address = address!("0x4e59b44847b379578588920ca78fbf26c0b4956c"); + +/// The init-code of the default CREATE2 FACTORY widely used in community +/// Retrieved from https://github.com/Arachnid/deterministic-deployment-proxy +pub const CREATE2_FACTORY_CODE: &[u8] = + include_bytes!("canonical_singletons_bytecode/create2_factory.bin"); + +/// Deployer of the canonical Multicall3 contract's historical presigned deployment transaction. +/// NOTE: unlike the other three contracts here, this is not a Nick's-method placeholder-signature +/// deployment — it is a real (now publicly known to be compromised, per the project's own README) +/// ECDSA-signed transaction. The compromise is irrelevant to reproducing it here: we never sign +/// with this key, we only replay the sender+nonce+data tuple it produced to derive the same +/// address and bytecode. +/// Retrieved from https://github.com/mds1/multicall3 (README, "New Deployments" section). +pub const MULTICALL3_DEPLOYER: Address = address!("0x05f32B3cC3888453ff71B01135B34FF8e41263F2"); +/// Canonical Multicall3 contract address, deployed by [MULTICALL3_DEPLOYER] with nonce 0. +pub const MULTICALL3_ADDRESS: Address = address!("0xcA11bde05977b3631167028862bE2a173976CA11"); +/// Multicall3 init-code, extracted from the presigned deployment transaction's data field. +/// Retrieved from https://github.com/mds1/multicall3 (README, "New Deployments" section). +pub const MULTICALL3_CODE: &[u8] = include_bytes!("canonical_singletons_bytecode/multicall3.bin"); + +/// Deployer of the canonical ERC-2470 SingletonFactory contract (Nick's-method deployment). +/// Retrieved from https://github.com/ethereum/ercs/blob/master/ERCS/erc-2470.md +pub const SINGLETON_FACTORY_DEPLOYER: Address = + address!("0xBb6e024b9cFFACB947A71991E386681B1Cd1477D"); +/// Canonical ERC-2470 SingletonFactory contract address, deployed by +/// [SINGLETON_FACTORY_DEPLOYER] with nonce 0. +pub const SINGLETON_FACTORY_ADDRESS: Address = + address!("0xce0042B868300000d44A59004Da54A005ffdcf9f"); +/// ERC-2470 SingletonFactory init-code, extracted from the presigned deployment transaction's +/// data field. Retrieved from https://github.com/ethereum/ercs/blob/master/ERCS/erc-2470.md +pub const SINGLETON_FACTORY_CODE: &[u8] = + include_bytes!("canonical_singletons_bytecode/singleton_factory.bin"); + +/// Deployer of the canonical CreateX contract (Nick's-method-style presigned deployment). +/// Retrieved from https://github.com/pcaversaccio/createx, +/// `scripts/presigned-createx-deployment-transactions/signed_serialised_transaction_gaslimit_3000000_.json` +/// (the 3M-gas variant — the one that produced the address on all of CreateX's existing chain +/// deployments; the 25M/45M variants carry byte-identical init-code, differing only in gas +/// limit/signature for chains needing a higher gas ceiling). +pub const CREATEX_DEPLOYER: Address = address!("0xeD456e05CaAb11d66C4c797dD6c1D6f9A7F352b5"); +/// Canonical CreateX contract address, deployed by [CREATEX_DEPLOYER] with nonce 0. +pub const CREATEX_ADDRESS: Address = address!("0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"); +/// CreateX init-code, extracted from the presigned deployment transaction's data field. +/// Retrieved from https://github.com/pcaversaccio/createx, +/// `scripts/presigned-createx-deployment-transactions/signed_serialised_transaction_gaslimit_3000000_.json` +pub const CREATEX_CODE: &[u8] = include_bytes!("canonical_singletons_bytecode/createx.bin"); + +/// Deployer of the canonical ERC-1820 Registry contract (Nick's-method deployment). +/// Retrieved from https://github.com/ethereum/ercs/blob/master/ERCS/erc-1820.md +pub const ERC1820_REGISTRY_DEPLOYER: Address = + address!("0xa990077c3205cbDf861e17Fa532eeB069cE9fF96"); +/// Canonical ERC-1820 Registry contract address, deployed by [ERC1820_REGISTRY_DEPLOYER] with +/// nonce 0. +pub const ERC1820_REGISTRY_ADDRESS: Address = + address!("0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24"); +/// ERC-1820 Registry init-code, extracted from the presigned deployment transaction's data +/// field. Retrieved from https://github.com/ethereum/ercs/blob/master/ERCS/erc-1820.md +pub const ERC1820_REGISTRY_CODE: &[u8] = + include_bytes!("canonical_singletons_bytecode/erc1820_registry.bin"); + +/// Generates the Create2Factory (Arachnid deterministic-deployment-proxy) deployment transaction. +pub fn generate_create2_factory_transaction() -> GenesisTransaction { + GenesisTransaction::new( + CREATE2_FACTORY_OWNER, + 0, + 0, + CREATE2_FACTORY_CODE.to_owned(), + TxKind::Create, + Some(CREATE2_FACTORY_ADDRESS), + ) +} + +/// Generates the Multicall3 deployment transaction. +pub fn generate_multicall3_transaction() -> GenesisTransaction { + GenesisTransaction::create( + MULTICALL3_DEPLOYER, + MULTICALL3_CODE.to_owned(), + 0, + MULTICALL3_ADDRESS, + ) +} + +/// Generates the ERC-2470 SingletonFactory deployment transaction. +pub fn generate_singleton_factory_transaction() -> GenesisTransaction { + GenesisTransaction::create( + SINGLETON_FACTORY_DEPLOYER, + SINGLETON_FACTORY_CODE.to_owned(), + 0, + SINGLETON_FACTORY_ADDRESS, + ) +} + +/// Generates the CreateX deployment transaction. +pub fn generate_createx_transaction() -> GenesisTransaction { + GenesisTransaction::create( + CREATEX_DEPLOYER, + CREATEX_CODE.to_owned(), + 0, + CREATEX_ADDRESS, + ) +} + +/// Generates the ERC-1820 Registry deployment transaction. +pub fn generate_erc1820_registry_transaction() -> GenesisTransaction { + GenesisTransaction::create( + ERC1820_REGISTRY_DEPLOYER, + ERC1820_REGISTRY_CODE.to_owned(), + 0, + ERC1820_REGISTRY_ADDRESS, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use primitives::{b256, keccak256}; + + /// For a plain-CREATE deployment, the resulting contract address depends only on the + /// sender and nonce, not on the init-code. This asserts each fixed (deployer, address) + /// pair is self-consistent under standard CREATE address derivation, matching the + /// canonical address recovered from each contract's real historical deployment. + macro_rules! create_address_derivation_test { + ($name:ident, $deployer:expr, $address:expr) => { + #[test] + fn $name() { + assert_eq!($deployer.create(0), $address); + } + }; + } + + create_address_derivation_test!( + create2_factory_deployer_nonce_zero_produces_canonical_address, + CREATE2_FACTORY_OWNER, + CREATE2_FACTORY_ADDRESS + ); + create_address_derivation_test!( + multicall3_deployer_nonce_zero_produces_canonical_address, + MULTICALL3_DEPLOYER, + MULTICALL3_ADDRESS + ); + create_address_derivation_test!( + singleton_factory_deployer_nonce_zero_produces_canonical_address, + SINGLETON_FACTORY_DEPLOYER, + SINGLETON_FACTORY_ADDRESS + ); + create_address_derivation_test!( + createx_deployer_nonce_zero_produces_canonical_address, + CREATEX_DEPLOYER, + CREATEX_ADDRESS + ); + create_address_derivation_test!( + erc1820_registry_deployer_nonce_zero_produces_canonical_address, + ERC1820_REGISTRY_DEPLOYER, + ERC1820_REGISTRY_ADDRESS + ); + + // keccak256 hash checks: a tripwire against accidental truncation/corruption of the + // embedded init-code assets, strictly stronger than a byte-length check alone. + // + // For CreateX, the expected hash is not just self-verification: pcaversaccio/createx's + // README itself publishes it as the value the community is told to check before trusting + // a CreateX deployment on any chain ("we recommend verifying prior to interacting with + // CreateX on any chain, that the keccak256 hash of the broadcasted contract creation + // bytecode is 0x12ec8615..."), so this test also confirms our embedded asset matches that + // publicly-published value, not just that it hasn't changed since we extracted it. + // + // Arachnid's proxy, Multicall3, ERC-2470, and ERC-1820 don't publish an init-code hash in + // their own docs/spec the way CreateX does, so those four hashes below are self-computed + // (via this codebase's own `keccak256`) at the time each asset was extracted and pinned as + // a regression guard, not independently-published community values. + #[test] + fn create2_factory_code_matches_expected_hash() { + assert_eq!( + keccak256(CREATE2_FACTORY_CODE), + b256!("50ea9137a35a9ad33b0ed4a431e9b6996ea9ed1f14781126cec78f168c0e64e5") + ); + } + + #[test] + fn multicall3_code_matches_expected_hash() { + assert_eq!( + keccak256(MULTICALL3_CODE), + b256!("0b2046aa018109118d518235014ac2c679dcbdff32c64705fdf50d048cd32d22") + ); + } + + #[test] + fn singleton_factory_code_matches_expected_hash() { + assert_eq!( + keccak256(SINGLETON_FACTORY_CODE), + b256!("122b6b28aeddfd05fa3ce4348e93d357b3ce50d9ab7dda4e8ee524a5b9a6ab3b") + ); + } + + #[test] + fn createx_code_matches_published_hash() { + // Retrieved from https://github.com/pcaversaccio/createx README: "the keccak256 hash + // of the broadcasted contract creation bytecode is + // 0x12ec861579b63a3ab9db3b5a23c57d56402ad3061475b088f17054e2f2daf22f". + assert_eq!( + keccak256(CREATEX_CODE), + b256!("12ec861579b63a3ab9db3b5a23c57d56402ad3061475b088f17054e2f2daf22f") + ); + } + + #[test] + fn erc1820_registry_code_matches_expected_hash() { + assert_eq!( + keccak256(ERC1820_REGISTRY_CODE), + b256!("141438dfbe77ba1a065eadf0ec62a4c90afa1c6355bc528d0f995015db252993") + ); + } + + #[test] + fn generate_create2_factory_transaction_matches_constants() { + let txn = generate_create2_factory_transaction(); + assert_eq!(*txn.sender(), CREATE2_FACTORY_OWNER); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(CREATE2_FACTORY_ADDRESS)); + assert_eq!(txn.data().as_slice(), CREATE2_FACTORY_CODE); + } + + #[test] + fn generate_multicall3_transaction_matches_constants() { + let txn = generate_multicall3_transaction(); + assert_eq!(*txn.sender(), MULTICALL3_DEPLOYER); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(MULTICALL3_ADDRESS)); + assert_eq!(txn.data().as_slice(), MULTICALL3_CODE); + } + + #[test] + fn generate_singleton_factory_transaction_matches_constants() { + let txn = generate_singleton_factory_transaction(); + assert_eq!(*txn.sender(), SINGLETON_FACTORY_DEPLOYER); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(SINGLETON_FACTORY_ADDRESS)); + assert_eq!(txn.data().as_slice(), SINGLETON_FACTORY_CODE); + } + + #[test] + fn generate_createx_transaction_matches_constants() { + let txn = generate_createx_transaction(); + assert_eq!(*txn.sender(), CREATEX_DEPLOYER); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(CREATEX_ADDRESS)); + assert_eq!(txn.data().as_slice(), CREATEX_CODE); + } + + #[test] + fn generate_erc1820_registry_transaction_matches_constants() { + let txn = generate_erc1820_registry_transaction(); + assert_eq!(*txn.sender(), ERC1820_REGISTRY_DEPLOYER); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(ERC1820_REGISTRY_ADDRESS)); + assert_eq!(txn.data().as_slice(), ERC1820_REGISTRY_CODE); + } +} diff --git a/crates/supra-extension/src/contracts/canonical_singletons_bytecode/create2_factory.bin b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/create2_factory.bin new file mode 100644 index 0000000000..8cb67ba8eb Binary files /dev/null and b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/create2_factory.bin differ diff --git a/crates/supra-extension/src/contracts/canonical_singletons_bytecode/createx.bin b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/createx.bin new file mode 100644 index 0000000000..36ef932d6a Binary files /dev/null and b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/createx.bin differ diff --git a/crates/supra-extension/src/contracts/canonical_singletons_bytecode/erc1820_registry.bin b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/erc1820_registry.bin new file mode 100644 index 0000000000..b26b2e5730 Binary files /dev/null and b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/erc1820_registry.bin differ diff --git a/crates/supra-extension/src/contracts/canonical_singletons_bytecode/multicall3.bin b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/multicall3.bin new file mode 100644 index 0000000000..19e2ed6d10 Binary files /dev/null and b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/multicall3.bin differ diff --git a/crates/supra-extension/src/contracts/canonical_singletons_bytecode/singleton_factory.bin b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/singleton_factory.bin new file mode 100644 index 0000000000..a8daae9bd5 Binary files /dev/null and b/crates/supra-extension/src/contracts/canonical_singletons_bytecode/singleton_factory.bin differ diff --git a/crates/supra-extension/src/contracts/configs.rs b/crates/supra-extension/src/contracts/configs.rs new file mode 100644 index 0000000000..d915638d67 --- /dev/null +++ b/crates/supra-extension/src/contracts/configs.rs @@ -0,0 +1,652 @@ +//! Configurations to generate genesis transactions + +use crate::transactions::block_metadata::DEFAULT_BLOCK_METADATA_GAS_LIMIT; +use primitives::supra_constants::is_supra_reserved; +use primitives::Address; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +/// Maximum number of automation tasks that the registry can hold. +/// The limit is deduced by running a benchmark for `monitorCycleEnd` automation registry function +/// which tracks automation cycle end and prepares the transaction state for graceful cycle transaction handling. +/// It is registered to be executed as part of the `BlockMeta::blockPrologue`. +/// The internal system `BlockMetadata` transaction generated and executed by consensus layer +/// specifies the gas limit for it to be [`DEFAULT_BLOCK_METADATA_GAS_LIMIT`]. Taking into account the fact the +/// registered entries are limited with gas-cap in scope of `BlockMeta::blockPrologue`, +/// the results of the benchmark and need to keep buffer for future entries of the `BlockMeta::blockPrologue` +/// the limit of 200 tasks is specified. +/// +/// `monitorCycleEnd`'s cost is O(n) in the live task count, independent of task ordering +/// (`LibCore.buildAliveOrderedTaskIds` compacts an append-only, always-ascending task-ID +/// list — see that function's NatSpec in `solidity/supra_contracts/src/libraries/LibCore.sol`). +/// See issue #3445 for background. +/// +// ┌───────────┬──────────────────────────────┐ +// │ Tasks (N) │ Gas used │ +// ├───────────┼──────────────────────────────┤ +// │ 50 │ 1,217,207 │ +// ├───────────┼──────────────────────────────┤ +// │ 100 │ 2,372,173 │ +// ├───────────┼──────────────────────────────┤ +// │ 150 │ 3,527,149 │ +// ├───────────┼──────────────────────────────┤ +// │ 200 │ 4,683,155 │ +// ├───────────┼──────────────────────────────┤ +// │ 250 │ 5,838,151 │ +// ├───────────┼──────────────────────────────┤ +// │ 300 │ 6,993,156 │ +// ├───────────┼──────────────────────────────┤ +// │ 350 │ 8,148,171 │ +// ├───────────┼──────────────────────────────┤ +// │ 800 │ 18,543,746 ⚠️ exceeds budget │ +// └───────────┴──────────────────────────────┘ +// +// (Figures from `forge test --match-contract MonitorCycleEndGasTest -vv` in +// solidity/supra_contracts/test/MonitorCycleEndGas.t.sol. That same run's +// `testMonitorCycleEndGas_BoundaryScan` binary-searches the exact safe ceiling: 723 tasks +// stay under BLOCK_METADATA_GAS_LIMIT, 724 exceeds it. `testMonitorCycleEndGas_BoundaryScan_ReverseSorted` +// confirms the same 723/724 boundary holds regardless of task ordering. +// 200 is kept far below that ceiling deliberately, as buffer for other future +// `BlockMeta::blockPrologue` entries and for the 63/64 forwarding-rule margin +// applied on top of BLOCK_METADATA_GAS_LIMIT (see `GenesisTransactionGeneratorConfig::is_valid`). +pub const MAX_SUPPORTED_AUTOMATION_TASKS: u16 = 200; + +/// Default maximum allowable duration (in seconds) from the registration time that a user +/// automation task can run. Set to 7 days. +pub const DEFAULT_TASK_DURATION_CAP_SECS: u64 = 604800; +/// Default maximum gas allocation for automation tasks per cycle. +pub const DEFAULT_REGISTRY_MAX_GAS_CAP: u128 = 8_000_000; +/// Default base fee per second for the full capacity of the automation registry, measured in +/// wei/sec. Equivalent to 0.004 SUPRA normalized based on the supra denominator between move +/// and evm currency. +pub const DEFAULT_AUTOMATION_BASE_FEE_WEI_PER_SEC: u128 = 1_714_530_600_000; +/// Default flat registration fee charged for each task. Equivalent to 0.05 SUPRA normalized +/// based on the supra denominator between move and evm currency. +pub const DEFAULT_FLAT_REGISTRATION_FEE_WEI: u128 = 21_431_633_000_000; +/// Default percentage representing the acceptable upper limit of committed gas amount relative +/// to `registry_max_gas_cap`. +pub const DEFAULT_CONGESTION_THRESHOLD_PERCENTAGE: u8 = 50; +/// Default base fee per second for the full capacity of the automation registry when the +/// congestion threshold is exceeded. Equivalent to 0.004 SUPRA normalized based on the supra +/// denominator between move and evm currency. +pub const DEFAULT_CONGESTION_BASE_FEE_WEI_PER_SEC: u128 = 1_714_530_600_000; +/// Default exponent that the congestion fee increases by exponentially. +pub const DEFAULT_CONGESTION_EXPONENT: u8 = 6; +/// Default governance-owned ceiling on `congestion_exponent`. Raising `congestion_exponent` +/// above this value requires first raising this cap in a separate config update — deliberate +/// friction on a parameter whose effect on the congestion fee is exponential. +pub const DEFAULT_MAX_CONGESTION_EXPONENT: u8 = 6; +/// Default maximum number of tasks that the registry can hold. +/// `task_capacity + sys_task_capacity` must not exceed [`MAX_SUPPORTED_AUTOMATION_TASKS`]. +pub const DEFAULT_TASK_CAPACITY: u16 = 160; +/// Default automation cycle duration in seconds. +pub const DEFAULT_CYCLE_DURATION_SECS: u64 = 600; +/// Default maximum allowable duration (in seconds) from the registration time that a system +/// automation task can run. Set to ~1 month. +pub const DEFAULT_SYS_TASK_DURATION_CAP_SECS: u64 = 2626560; +/// Default maximum gas allocation for system automation tasks per cycle. +pub const DEFAULT_SYS_REGISTRY_MAX_GAS_CAP: u128 = 2_000_000; +/// Default maximum number of system tasks that the registry can hold. +pub const DEFAULT_SYS_TASK_CAPACITY: u16 = 40; +/// Default flag indicating whether the automation feature is enabled at startup. +pub const DEFAULT_ENABLE_AUTOMATION_FEATURE: bool = true; + +/// Configuration parameters for Automation Registry contracts initialization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutomationRegistryConfigV1 { + /// Maximum allowable duration (in seconds) from the registration time that a user automation task can run. + pub task_duration_cap_secs: u64, + /// Maximum gas allocation for automation tasks per cycle. + pub registry_max_gas_cap: u128, + /// Base fee per second for the full capacity of the automation registry, measured in wei/sec. + pub automation_base_fee_wei_per_sec: u128, + /// Flat registration fee charged by default for each task. + pub flat_registration_fee_wei: u128, + /// Percentage representing the acceptable upper limit of committed gas amount relative to registry_max_gas_cap. + pub congestion_threshold_percentage: u8, + /// Base fee per second for the full capacity of the automation registry when the congestion threshold is exceeded. + pub congestion_base_fee_wei_per_sec: u128, + /// The congestion fee increases exponentially based on this value. + pub congestion_exponent: u8, + /// Governance-owned upper bound on `congestion_exponent`. Must be non-zero and + /// `congestion_exponent` must not exceed it. + pub max_congestion_exponent: u8, + /// Maximum number of tasks that the registry can hold. + pub task_capacity: u16, + /// Automation cycle duration in seconds. + pub cycle_duration_secs: u64, + /// Maximum allowable duration (in seconds) from the registration time that a system automation task can run. + pub sys_task_duration_cap_secs: u64, + /// Maximum gas allocation for system automation tasks per cycle. + pub sys_registry_max_gas_cap: u128, + /// Maximum number of system tasks that the registry can hold. + pub sys_task_capacity: u16, + /// Indicates whether the automation feature is enabled at startup + pub enable_automation_feature: bool, +} + +impl AutomationRegistryConfigV1 { + /// Checks whether the config is valid to create non-failable transactions. + pub fn is_valid(&self) -> Result<(), anyhow::Error> { + if self.task_duration_cap_secs == 0 || self.sys_task_duration_cap_secs == 0 { + return Err(anyhow::anyhow!( + "[System] Task duration cap must be positive" + )); + } + if self.registry_max_gas_cap == 0 || self.sys_registry_max_gas_cap == 0 { + return Err(anyhow::anyhow!( + "[System] Registry max gas cap must be positive" + )); + } + if self.cycle_duration_secs > self.task_duration_cap_secs + || self.cycle_duration_secs > self.sys_task_duration_cap_secs + { + return Err(anyhow::anyhow!( + "[System] Task duration cap should be greater than cycle duration" + )); + } + if self.congestion_threshold_percentage > 100 { + return Err(anyhow::anyhow!( + "Congestion threshold percentage should be less or equal to 100" + )); + } + if self.sys_task_capacity == 0 || self.task_capacity == 0 { + return Err(anyhow::anyhow!("Task capacity cannot be 0")); + } + if self.congestion_exponent == 0 { + return Err(anyhow::anyhow!("Congestion exponent cannot be 0")); + } + if self.max_congestion_exponent == 0 { + return Err(anyhow::anyhow!("Max congestion exponent cannot be 0")); + } + if self.congestion_exponent > self.max_congestion_exponent { + return Err(anyhow::anyhow!( + "Congestion exponent cannot exceed max congestion exponent" + )); + } + if self.sys_task_capacity.saturating_add(self.task_capacity) + > MAX_SUPPORTED_AUTOMATION_TASKS + { + return Err(anyhow::anyhow!( + "Total supported task capacity exceeded: {MAX_SUPPORTED_AUTOMATION_TASKS}" + )); + } + Ok(()) + } +} + +impl Default for AutomationRegistryConfigV1 { + fn default() -> Self { + Self { + task_duration_cap_secs: DEFAULT_TASK_DURATION_CAP_SECS, + registry_max_gas_cap: DEFAULT_REGISTRY_MAX_GAS_CAP, + automation_base_fee_wei_per_sec: DEFAULT_AUTOMATION_BASE_FEE_WEI_PER_SEC, + flat_registration_fee_wei: DEFAULT_FLAT_REGISTRATION_FEE_WEI, + congestion_threshold_percentage: DEFAULT_CONGESTION_THRESHOLD_PERCENTAGE, + congestion_base_fee_wei_per_sec: DEFAULT_CONGESTION_BASE_FEE_WEI_PER_SEC, + congestion_exponent: DEFAULT_CONGESTION_EXPONENT, + max_congestion_exponent: DEFAULT_MAX_CONGESTION_EXPONENT, + task_capacity: DEFAULT_TASK_CAPACITY, + cycle_duration_secs: DEFAULT_CYCLE_DURATION_SECS, + sys_task_duration_cap_secs: DEFAULT_SYS_TASK_DURATION_CAP_SECS, + sys_registry_max_gas_cap: DEFAULT_SYS_REGISTRY_MAX_GAS_CAP, + sys_task_capacity: DEFAULT_SYS_TASK_CAPACITY, + enable_automation_feature: DEFAULT_ENABLE_AUTOMATION_FEATURE, + } + } +} + +/// Configuration parameters for Automation Registry contracts initialization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AutomationRegistryConfig { + /// First version of the evm automation registry contract configurations + V1(AutomationRegistryConfigV1), +} + +impl AutomationRegistryConfig { + /// Returns [AutomationRegistryConfigV1] if the variant is [Self::V1] + pub fn v1(&self) -> Option<&AutomationRegistryConfigV1> { + let Self::V1(config) = self; + Some(config) + } + + /// Checks validity of automation registry configuration. + pub fn is_valid(&self) -> Result<(), anyhow::Error> { + let Self::V1(v1) = self; + v1.is_valid() + } +} + +impl From for AutomationRegistryConfig { + fn from(config: AutomationRegistryConfigV1) -> Self { + Self::V1(config) + } +} + +/// Genesis Transaction generator configuration details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GenesisTransactionGeneratorConfig { + /// List of EOAs to set up multisig foundation wallet. + pub foundation_owners: Vec
, + /// Threshold of the foundation multisig wallet. + pub foundation_threshold: u64, + /// Flag indicating whether full set of genesis transaction should be generated or only mandatory once. + pub full_set: bool, + #[serde(skip_serializing_if = "Option::is_none")] + /// Automation configuration parameters (optional, uses defaults if None). + pub automation_config: Option, + /// Gas cap for block-prologue/block-metadata transaction. + pub block_prologue_gas_cap: u64, +} + +impl GenesisTransactionGeneratorConfig { + /// Checks whether the config is valid to create non-failable transactions. + pub fn is_valid(&self) -> Result<(), anyhow::Error> { + if self.block_prologue_gas_cap == 0 { + return Err(anyhow::anyhow!("Block prologue gas cap must be positive")); + } + // Cap the block prologue gas cap with [`BLOCK_METADATA_GAS_LIMIT`] of the initial release of SEVM. + if self.block_prologue_gas_cap > DEFAULT_BLOCK_METADATA_GAS_LIMIT { + return Err(anyhow::anyhow!("Block prologue gas cap must not exceed the default BlockMetadata GasLimit ({DEFAULT_BLOCK_METADATA_GAS_LIMIT})")); + } + if self.foundation_owners.is_empty() { + return Err(anyhow::anyhow!("Foundation owners must be provided")); + } + if self.foundation_threshold == 0 { + return Err(anyhow::anyhow!("Foundation threshold cannot be 0")); + } + if self.foundation_threshold > self.foundation_owners.len() as u64 { + return Err(anyhow::anyhow!( + "Foundation threshold must be less or equal the number of owners" + )); + } + let mut seen_owners = HashSet::with_capacity(self.foundation_owners.len()); + for owner in &self.foundation_owners { + if !seen_owners.insert(owner) { + return Err(anyhow::anyhow!("Foundation owners must be unique")); + } + if owner.is_zero() || is_supra_reserved(owner) { + return Err(anyhow::anyhow!( + "Foundation owner address cannot be zero or supra reserved: {owner:?}" + )); + } + } + if let Some(automation_config) = &self.automation_config { + automation_config.is_valid()?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use primitives::supra_constants::u64_to_address; + + fn owners(n: u64) -> Vec
{ + (1..=n).map(u64_to_address).collect() + } + + /// A hand-picked config satisfying every `AutomationRegistryConfigV1::is_valid` rule, + /// including `task_capacity + sys_task_capacity <= MAX_SUPPORTED_AUTOMATION_TASK`. + fn valid_automation_config() -> AutomationRegistryConfigV1 { + AutomationRegistryConfigV1 { + task_capacity: 100, + sys_task_capacity: 50, + ..AutomationRegistryConfigV1::default() + } + } + + fn valid_genesis_config() -> GenesisTransactionGeneratorConfig { + GenesisTransactionGeneratorConfig { + foundation_owners: owners(3), + foundation_threshold: 2, + full_set: false, + automation_config: None, + block_prologue_gas_cap: 100_000, + } + } + + // --- AutomationRegistryConfigV1::is_valid --- + + #[test] + fn valid_automation_config_is_accepted() { + assert!(valid_automation_config().is_valid().is_ok()); + } + + /// `Default` must stay within `MAX_SUPPORTED_AUTOMATION_TASK` so that + /// `AutomationRegistryConfigV1::default()` is always a valid config out of the box. + #[test] + fn default_automation_config_is_valid() { + assert!(AutomationRegistryConfigV1::default().is_valid().is_ok()); + } + + #[test] + fn zero_task_duration_cap_secs_is_rejected() { + let config = AutomationRegistryConfigV1 { + task_duration_cap_secs: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_sys_task_duration_cap_secs_is_rejected() { + let config = AutomationRegistryConfigV1 { + sys_task_duration_cap_secs: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_registry_max_gas_cap_is_rejected() { + let config = AutomationRegistryConfigV1 { + registry_max_gas_cap: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_sys_registry_max_gas_cap_is_rejected() { + let config = AutomationRegistryConfigV1 { + sys_registry_max_gas_cap: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn cycle_duration_exceeding_task_duration_cap_is_rejected() { + let config = AutomationRegistryConfigV1 { + task_duration_cap_secs: 100, + cycle_duration_secs: 101, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn cycle_duration_exceeding_sys_task_duration_cap_is_rejected() { + let config = AutomationRegistryConfigV1 { + sys_task_duration_cap_secs: 100, + cycle_duration_secs: 101, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn cycle_duration_equal_to_task_duration_cap_is_accepted() { + let config = AutomationRegistryConfigV1 { + task_duration_cap_secs: 100, + sys_task_duration_cap_secs: 100, + cycle_duration_secs: 100, + ..valid_automation_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn congestion_threshold_percentage_over_100_is_rejected() { + let config = AutomationRegistryConfigV1 { + congestion_threshold_percentage: 101, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn congestion_threshold_percentage_of_100_is_accepted() { + let config = AutomationRegistryConfigV1 { + congestion_threshold_percentage: 100, + ..valid_automation_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn zero_task_capacity_is_rejected() { + let config = AutomationRegistryConfigV1 { + task_capacity: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_sys_task_capacity_is_rejected() { + let config = AutomationRegistryConfigV1 { + sys_task_capacity: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_congestion_exponent_is_rejected() { + let config = AutomationRegistryConfigV1 { + congestion_exponent: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_max_congestion_exponent_is_rejected() { + let config = AutomationRegistryConfigV1 { + max_congestion_exponent: 0, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn congestion_exponent_exceeding_max_is_rejected() { + let config = AutomationRegistryConfigV1 { + congestion_exponent: 7, + max_congestion_exponent: 6, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn congestion_exponent_equal_to_max_is_accepted() { + let config = AutomationRegistryConfigV1 { + congestion_exponent: 6, + max_congestion_exponent: 6, + ..valid_automation_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn task_capacity_sum_exceeding_max_supported_is_rejected() { + let config = AutomationRegistryConfigV1 { + task_capacity: 150, + sys_task_capacity: 51, + ..valid_automation_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn task_capacity_sum_equal_to_max_supported_is_accepted() { + let config = AutomationRegistryConfigV1 { + task_capacity: 150, + sys_task_capacity: 50, + ..valid_automation_config() + }; + assert!(config.is_valid().is_ok()); + } + + // --- AutomationRegistryConfig::is_valid (V1 wrapper delegation) --- + + #[test] + fn config_enum_delegates_to_v1_valid_case() { + let config: AutomationRegistryConfig = valid_automation_config().into(); + assert!(config.is_valid().is_ok()); + } + + #[test] + fn config_enum_delegates_to_v1_invalid_case() { + let v1 = AutomationRegistryConfigV1 { + registry_max_gas_cap: 0, + ..valid_automation_config() + }; + let config: AutomationRegistryConfig = v1.into(); + assert!(config.is_valid().is_err()); + } + + // --- GenesisTransactionGeneratorConfig::is_valid --- + + #[test] + fn valid_genesis_config_without_automation_is_accepted() { + assert!(valid_genesis_config().is_valid().is_ok()); + } + + #[test] + fn zero_block_prologue_gas_cap_is_rejected() { + let config = GenesisTransactionGeneratorConfig { + block_prologue_gas_cap: 0, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn block_prologue_gas_cap_at_63_64_boundary_is_accepted() { + let upper_bound = (DEFAULT_BLOCK_METADATA_GAS_LIMIT as u128) * 63 / 64; + let config = GenesisTransactionGeneratorConfig { + block_prologue_gas_cap: upper_bound as u64, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn block_prologue_gas_cap_above_block_metadata_gas_limit_is_rejected() { + let upper_bound = DEFAULT_BLOCK_METADATA_GAS_LIMIT; + let config = GenesisTransactionGeneratorConfig { + block_prologue_gas_cap: (upper_bound + 1) as u64, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn empty_foundation_owners_is_rejected() { + let config = GenesisTransactionGeneratorConfig { + foundation_owners: vec![], + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn foundation_threshold_above_owner_count_is_rejected() { + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners(3), + foundation_threshold: 4, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn foundation_threshold_equal_to_owner_count_is_accepted() { + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners(3), + foundation_threshold: 3, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn foundation_threshold_below_owner_count_is_accepted() { + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners(3), + foundation_threshold: 1, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn zero_foundation_threshold_is_rejected() { + let config = GenesisTransactionGeneratorConfig { + foundation_threshold: 0, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn duplicate_foundation_owners_is_rejected() { + let mut duplicated_owners = owners(3); + duplicated_owners[1] = duplicated_owners[0]; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: duplicated_owners, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn zero_address_foundation_owner_is_rejected() { + let mut owners_with_zero = owners(3); + owners_with_zero[1] = Address::ZERO; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners_with_zero, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn supra_reserved_foundation_owner_is_rejected() { + let mut owners_with_reserved = owners(3); + owners_with_reserved[1] = primitives::supra_constants::VM_SIGNER; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners_with_reserved, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } + + #[test] + fn foundation_owner_just_outside_reserved_range_is_accepted() { + let mut owners_at_boundary = owners(3); + owners_at_boundary[1] = u64_to_address(0x5355_5100); + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners_at_boundary, + ..valid_genesis_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn valid_genesis_config_with_valid_automation_is_accepted() { + let config = GenesisTransactionGeneratorConfig { + automation_config: Some(valid_automation_config().into()), + ..valid_genesis_config() + }; + assert!(config.is_valid().is_ok()); + } + + #[test] + fn invalid_automation_config_propagates_error() { + let bad_automation = AutomationRegistryConfigV1 { + congestion_exponent: 0, + ..valid_automation_config() + }; + let config = GenesisTransactionGeneratorConfig { + automation_config: Some(bad_automation.into()), + ..valid_genesis_config() + }; + assert!(config.is_valid().is_err()); + } +} diff --git a/crates/supra-extension/src/contracts/generator.rs b/crates/supra-extension/src/contracts/generator.rs new file mode 100644 index 0000000000..c70ed89283 --- /dev/null +++ b/crates/supra-extension/src/contracts/generator.rs @@ -0,0 +1,978 @@ +//! Encloses transaction data generation logic based on the genesis contracts + +use crate::contracts::canonical_singletons; +use crate::contracts::configs::{AutomationRegistryConfig, GenesisTransactionGeneratorConfig}; +use crate::contracts::transaction::{GenesisTransaction, GenesisTransactionTags}; +use alloy::primitives::Address; +use alloy_sol_types::{sol, SolCall, SolConstructor}; +use anyhow::{anyhow, Result}; +use bincode::config; +use derive_getters::Getters; +use once_cell::sync::Lazy; +use primitives::supra_constants::VM_SIGNER; +use primitives::{Bytes, U256}; +use std::collections::BTreeMap; + +/// Load precompiled combined bytecode of contracts. +const CONTRACT_BYTECODES_RAW: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/supra_contracts_bytecode.bin")); + +static CONTRACT_BYTECODES: Lazy>> = Lazy::new(|| { + // Deserialize the bytecodes from the raw bytes + let (bytecodes, _) = + bincode::serde::decode_from_slice(CONTRACT_BYTECODES_RAW, config::standard()) + .expect("Failed to deserialize contract bytecodes"); + bytecodes +}); + +/////////////// Multi-Signature-Wallet related contracts and init APIs ///////////////////////////// +const MULTISIG_WALLET: &str = "MultiSignatureWallet"; +const MULTISIG_BEACON: &str = "MultisigBeacon"; +const BEACON_PROXY: &str = "BeaconProxy"; + +sol! { + contract MultiSignatureWallet { + function initialize(address[] memory _owners, uint256 _numConfirmationsRequired); + } + + contract MultisigBeacon { + constructor(address _implementation, address _owner); + } + + contract BeaconProxy { + constructor(address _beacon, bytes _data); + } +} + +///////////////////// ERC20Supra related contracts and init APIs ///////////////////////////// +const ERC20_SUPRA: &str = "ERC20Supra"; +const ERC20_SUPRA_HANDLER: &str = "ERC20SupraHandler"; +sol! { + contract ERC20Supra { + function initialize(address _initialOwner, address[] memory _authorizedAddresses); + } + + contract ERC20SupraHandler { + function initialize(address _initialOwner, address _erc20Supra); + } +} + +///////////////////// Block Meta related contracts and init APIs ///////////////////////////// +const BLOCK_META: &str = "BlockMeta"; +sol! { + contract BlockMeta { + function initialize(address _initialOwner, uint64 _gasCap); + } +} + +const ERC1967PROXY: &str = "ERC1967Proxy"; + +sol! { + contract ERC1967Proxy { + constructor(address _impl, bytes _data); + } +} + +///////////////////// Automation related contracts and init APIs ///////////////////////////// + +const DIAMOND_CUT_FACET: &str = "DiamondCutFacet"; +const DIAMOND: &str = "Diamond"; +const DIAMOND_LOUPE_FACET: &str = "DiamondLoupeFacet"; +const OWNERSHIP_FACET: &str = "OwnershipFacet"; +const CONFIG_FACET: &str = "ConfigFacet"; +const REGISTRY_FACET: &str = "RegistryFacet"; +const CORE_FACET: &str = "CoreFacet"; +const DIAMOND_INIT: &str = "DiamondInit"; + +sol! { + + /// Initialization parameters for Automation Registry State. + struct InitParams { + uint64 taskDurationCapSecs; + uint128 registryMaxGasCap; + uint128 automationBaseFeeWeiPerSec; + uint128 flatRegistrationFeeWei; + uint8 congestionThresholdPercentage; + uint128 congestionBaseFeeWeiPerSec; + uint8 congestionExponent; + uint8 maxCongestionExponent; + uint16 taskCapacity; + uint64 cycleDurationSecs; + uint64 sysTaskDurationCapSecs; + uint128 sysRegistryMaxGasCap; + uint16 sysTaskCapacity; + bool automationEnabled; + bool registrationEnabled; + } + + /// Addresses of the facets for diamond cut and diamond initializer contract + struct FacetsDeployment { + address diamondCutFacet; + address loupeFacet; + address ownershipFacet; + address configFacet; + address registryFacet; + address coreFacet; + address diamondInit; + } + + contract Diamond { + constructor( + address _contractOwner, + FacetsDeployment memory _facets, + address _erc20Supra, + InitParams memory _params + ); + } + +} + +/// Genesis Transaction generator using configured address as transaction owner. +/// It provides means to generate minimal mandatory set of genesis transactions to set up evm state, +/// and conditionally generates non-mandatory set of transactions. +#[derive(Debug, Getters)] +pub struct GenesisTransactionGenerator { + nonce: u64, + address: Address, +} + +impl Default for GenesisTransactionGenerator { + fn default() -> Self { + Self::new(VM_SIGNER) + } +} + +impl GenesisTransactionGenerator { + fn new(address: Address) -> Self { + Self::new_with_nonce(address, 0) + } + + fn new_with_nonce(address: Address, nonce: u64) -> Self { + Self { nonce, address } + } + + /// Prepares genesis transactions based on the input configuration. + pub fn prepare_genesis_transactions( + &mut self, + config: GenesisTransactionGeneratorConfig, + ) -> Result> { + config.is_valid()?; + let GenesisTransactionGeneratorConfig { + foundation_owners, + foundation_threshold, + full_set, + automation_config, + block_prologue_gas_cap, + } = config; + // First, the Create2 Factory contract deployment, which will allow later to utilize + // create2 API if required during genesis, alongside the other canonical EVM singleton + // predeploys (Multicall3, ERC-2470 SingletonFactory, CreateX, ERC-1820 Registry) that + // the wider EVM ecosystem/tooling expects at fixed addresses. All five are unconditional, + // independent of `full_set`, since none relate to Supra's own application contracts below. + let mut genesis_transactions = BTreeMap::from([ + ( + GenesisTransactionTags::Create2Factory, + canonical_singletons::generate_create2_factory_transaction(), + ), + ( + GenesisTransactionTags::Multicall3, + canonical_singletons::generate_multicall3_transaction(), + ), + ( + GenesisTransactionTags::SingletonFactory, + canonical_singletons::generate_singleton_factory_transaction(), + ), + ( + GenesisTransactionTags::CreateX, + canonical_singletons::generate_createx_transaction(), + ), + ( + GenesisTransactionTags::Erc1820Registry, + canonical_singletons::generate_erc1820_registry_transaction(), + ), + ]); + // Second multisig contract and foundation multisig account setup should be done + genesis_transactions + .extend(self.setup_multisig_wallet(foundation_owners, foundation_threshold)?); + if full_set { + let multisig_address = *genesis_transactions + .get(&GenesisTransactionTags::FoundationWallet) + .expect("Foundation Wallet deployment transaction") + .deploy_address() + .as_ref() + .expect("Foundation wallet deployment address should be set"); + + // Erc20 Supra contracts + let erc20_contracts = self.setup_erc20_contracts(multisig_address)?; + let erc20supra_address = *erc20_contracts + .get(&GenesisTransactionTags::Erc20Supra) + .expect("Erc20Supra deployment transaction exists") + .deploy_address() + .as_ref() + .expect("Erc20Supra deployment address should be set"); + genesis_transactions.extend(erc20_contracts); + + // BlockMetadata contract + genesis_transactions.extend( + self.setup_block_metadata(multisig_address, block_prologue_gas_cap)? + .into_iter(), + ); + + // Automation registry contracts + if let Some(config) = automation_config { + genesis_transactions.extend( + self.setup_automation_registry(multisig_address, erc20supra_address, config)? + .into_iter(), + ); + } + }; + + Ok(genesis_transactions) + } + + /// Generates genesis transactions for foundation multisig wallet deployment. + /// Deployment order follows GenesisTransactionTags: + /// 1. MultisigWalletImpl (0) - MultiSignatureWallet implementation contract + /// 2. MultisigBeacon (1) - Beacon contract pointing to implementation + /// 3. FoundationWallet (2) - BeaconProxy with initialize(owners, threshold) + /// + /// The beacon pattern allows future upgrades by changing the implementation + /// address in the beacon contract. The foundation wallet (BeaconProxy) will + /// automatically use the new implementation. + fn setup_multisig_wallet( + &mut self, + owners: Vec
, + threshold: u64, + ) -> Result> { + // ------------------------------------------------------------------------- + // Pre-compute all deployment addresses + // nonce+0: MultiSignatureWallet implementation + // nonce+1: MultisigBeacon + // nonce+2: BeaconProxy (Foundation Wallet) + // ------------------------------------------------------------------------- + let multisig_impl_address = self.address.create(self.nonce); + let beacon_contract_address = self.address.create(self.nonce + 1); + let multisig_wallet_address = self.address.create(self.nonce + 2); + + // ------------------------------------------------------------------------- + // 1. Deploy MultiSignatureWallet implementation (no constructor args) + // ------------------------------------------------------------------------- + let multisig_impl_create_data = Self::load_contract_bytecode(MULTISIG_WALLET)?; + let multisig_txn = GenesisTransaction::create( + self.address.clone(), + multisig_impl_create_data, + self.nonce, + multisig_impl_address, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 2. Deploy MultisigBeacon + // Constructor args: implementation address, owner (the wallet itself) + // The beacon owner is set to the wallet address for self-governance + // ------------------------------------------------------------------------- + let multisig_beacon_create_data = Self::load_contract_bytecode(MULTISIG_BEACON)?; + let beacon_args = MultisigBeacon::constructorCall { + _implementation: multisig_impl_address, + _owner: multisig_wallet_address, + } + .abi_encode(); + let beacon_txn_data = [multisig_beacon_create_data, beacon_args].concat(); + let multisig_beacon_txn = GenesisTransaction::create( + self.address.clone(), + beacon_txn_data, + self.nonce, + beacon_contract_address, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 3. Deploy BeaconProxy (Foundation Wallet) + // Constructor args: beacon address, initialization data + // Initialization data: initialize(owners[], threshold) + // ------------------------------------------------------------------------- + let beacon_proxy_create_data = Self::load_contract_bytecode(BEACON_PROXY)?; + // Encode the initialize call data for MultiSignatureWallet + let multisig_init_data = MultiSignatureWallet::initializeCall { + _owners: owners, + _numConfirmationsRequired: U256::from(threshold), + } + .abi_encode(); + // Encode the BeaconProxy constructor args + let beacon_proxy_args = BeaconProxy::constructorCall { + _beacon: beacon_contract_address, + _data: Bytes::from(multisig_init_data), + } + .abi_encode(); + // Concatenate bytecode + constructor args for deployment + let beacon_proxy_txn_data = [beacon_proxy_create_data, beacon_proxy_args].concat(); + let beacon_proxy_txn = GenesisTransaction::create( + self.address.clone(), + beacon_proxy_txn_data, + self.nonce, + multisig_wallet_address, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::MultisigWalletImpl, multisig_txn), + (GenesisTransactionTags::MultisigBeacon, multisig_beacon_txn), + (GenesisTransactionTags::FoundationWallet, beacon_proxy_txn), + ])) + } + + /// Generates Erc20Supra and Erc20SupraHandler contracts deployment transactions. + /// Both are ERC1967Proxy upgradeable contracts, and are deployed in the same transaction + /// batch to ensure correct initial authorization setup. + fn setup_erc20_contracts( + &mut self, + owner: Address, + ) -> Result> { + // Precomputed addresses + // nonce + 0: ERC20Supra Impl + // nonce + 1: ERC20Supra (Proxy) + // nonce + 2: ERC20SupraHandler Impl + // nonce + 3: ERC20SupraHandler (Proxy) + let erc20_supra_address = self.address.create(self.nonce + 1); + let erc20_handler_address = self.address.create(self.nonce + 3); + + // For now only erc20supra handler address is specified as authorized, the bridge one will be specified past deployment + let mut erc20_supra_txn = self.setup_erc20_supra(owner, vec![erc20_handler_address])?; + let gen_erc20_supra_address = *erc20_supra_txn + .get(&GenesisTransactionTags::Erc20Supra) + .expect("Erc20Supra should be deployed") + .deploy_address() + .as_ref() + .expect("Erc20Supra deploy address"); + assert_eq!( + erc20_supra_address, gen_erc20_supra_address, + "Address computed by tag and nonce should be the same" + ); + + let erc20_handler_txn = self.setup_erc20_supra_handler(owner, gen_erc20_supra_address)?; + let gen_erc20_handler_address = *erc20_handler_txn + .get(&GenesisTransactionTags::Erc20SupraHandler) + .expect("Erc20SupraHandler should be deployed") + .deploy_address() + .as_ref() + .expect("Erc20SupraHandler deploy address"); + + assert_eq!( + erc20_handler_address, gen_erc20_handler_address, + "Address computed by tag and nonce should be the same" + ); + + erc20_supra_txn.extend(erc20_handler_txn); + Ok(erc20_supra_txn) + } + + /// Generates genesis transaction for ERC20Supra token contract deployment. + /// Deployment order follows GenesisTransactionTags: + /// 1. ERC20SupraImpl (nonce+0) - ERC20Supra implementation contract (UUPS upgradeable) + /// 2. ERC20Supra (nonce+1) - ERC1967Proxy with initialize(initialOwner, authorizedAddresses[]) + /// + /// The initial owner (typically the foundation multisig wallet) receives + /// administrative privileges over the token contract. + fn setup_erc20_supra( + &mut self, + initial_owner: Address, + authorized_addresses: Vec
, + ) -> Result> { + // ------------------------------------------------------------------------- + // Pre-compute deployment address + // ------------------------------------------------------------------------- + let erc20_supra_address_impl = self.address.create(self.nonce); + let erc20_supra_address = self.address.create(self.nonce + 1); + + // ------------------------------------------------------------------------- + // 1. Deploy ERC20Supra + // ------------------------------------------------------------------------- + let erc20_contract_create_data = Self::load_contract_bytecode(ERC20_SUPRA)?; + let erc20supra_impl = GenesisTransaction::create( + self.address, + erc20_contract_create_data, + self.nonce, + erc20_supra_address_impl, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 2. Deploy ERC1967Proxy (ERC20Supra) + // Constructor args: implementation address, initialization data + // Initialization data: initialize(initialOwner, authorizedAddresses[]) + // ------------------------------------------------------------------------- + let proxy_impl_data = Self::load_contract_bytecode(ERC1967PROXY)?; + // Encode Erc20Supra initialize call + let erc20_init_args = ERC20Supra::initializeCall { + _initialOwner: initial_owner, + _authorizedAddresses: authorized_addresses, + } + .abi_encode(); + // Encode the ERC1967Proxy constructor args + let proxy_args = ERC1967Proxy::constructorCall { + _impl: erc20_supra_address_impl, + _data: erc20_init_args.into(), + } + .abi_encode(); + // Concatenate bytecode + constructor args for deployment + let proxy_txn_data = [proxy_impl_data, proxy_args].concat(); + let erc20supra = GenesisTransaction::create( + self.address, + proxy_txn_data, + self.nonce, + erc20_supra_address, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::Erc20SupraImpl, erc20supra_impl), + (GenesisTransactionTags::Erc20Supra, erc20supra), + ])) + } + + /// Generates genesis transaction for ERC20SupraHandler token conversion contract deployment. + /// Deployment order follows GenesisTransactionTags: + /// 1. ERC20SupraHandlerImpl (nonce+0) - ERC20SupraHandler implementation contract (UUPS upgradeable) + /// 2. ERC20SupraHandler (nonce+1) - ERC1967Proxy with initialize(initialOwner, erc20supra) + /// + /// The initial owner (typically the foundation multisig wallet) receives + /// administrative privileges over the token contract. + fn setup_erc20_supra_handler( + &mut self, + initial_owner: Address, + erc20supra: Address, + ) -> Result> { + // ------------------------------------------------------------------------- + // Pre-compute deployment address + // ------------------------------------------------------------------------- + let erc20_handler_address_impl = self.address.create(self.nonce); + let erc20_handler_address = self.address.create(self.nonce + 1); + + // ------------------------------------------------------------------------- + // 1. Deploy ERC20SupraHandler + // ------------------------------------------------------------------------- + let erc20_contract_create_data = Self::load_contract_bytecode(ERC20_SUPRA_HANDLER)?; + let erc20supra_handler_impl = GenesisTransaction::create( + self.address, + erc20_contract_create_data, + self.nonce, + erc20_handler_address_impl, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 2. Deploy ERC1967Proxy (ERC20SupraHandler) + // Constructor args: implementation address, initialization data + // Initialization data: initialize(initialOwner, erc20supra) + // ------------------------------------------------------------------------- + let proxy_impl_data = Self::load_contract_bytecode(ERC1967PROXY)?; + // Encode Erc20Supra initialize call + let erc20_handler_init_args = ERC20SupraHandler::initializeCall { + _initialOwner: initial_owner, + _erc20Supra: erc20supra, + } + .abi_encode(); + // Encode the ERC1967Proxy constructor args + let proxy_args = ERC1967Proxy::constructorCall { + _impl: erc20_handler_address_impl, + _data: erc20_handler_init_args.into(), + } + .abi_encode(); + // Concatenate bytecode + constructor args for deployment + let proxy_txn_data = [proxy_impl_data, proxy_args].concat(); + let erc20supra_handler = GenesisTransaction::create( + self.address, + proxy_txn_data, + self.nonce, + erc20_handler_address, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + ( + GenesisTransactionTags::Erc20SupraHandlerImpl, + erc20supra_handler_impl, + ), + ( + GenesisTransactionTags::Erc20SupraHandler, + erc20supra_handler, + ), + ])) + } + + /// Generates genesis transactions for BlockMeta contract deployment. + /// Deployment order follows GenesisTransactionTags: + /// 1. BlockMetadataImpl - BlockMeta implementation contract (UUPS upgradeable) + /// 2. BlockMetadata - ERC1967Proxy with initialize(initialOwner) + /// + /// BlockMeta is a UUPS upgradeable contract deployed behind an ERC1967Proxy. + /// The proxy pattern allows future upgrades while maintaining the same address. + fn setup_block_metadata( + &mut self, + initial_owner: Address, + gas_cap: u64, + ) -> Result> { + // ------------------------------------------------------------------------- + // Pre-compute all deployment addresses + // nonce+0: BlockMeta implementation + // nonce+1: ERC1967Proxy (BlockMetadata) + // ------------------------------------------------------------------------- + let block_metadata_impl_address = self.address.create(self.nonce); + let block_metadata_proxy_address = self.address.create(self.nonce + 1); + + // ------------------------------------------------------------------------- + // 1. Deploy BlockMeta implementation (UUPS - no constructor args) + // ------------------------------------------------------------------------- + let block_metadata_impl = Self::load_contract_bytecode(BLOCK_META)?; + let block_metadata_impl_txn = GenesisTransaction::create( + self.address, + block_metadata_impl, + self.nonce, + block_metadata_impl_address, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 2. Deploy ERC1967Proxy (BlockMetadata) + // Constructor args: implementation address, initialization data + // Initialization data: initialize(initialOwner, gasCap) + // ------------------------------------------------------------------------- + let proxy_impl_data = Self::load_contract_bytecode(ERC1967PROXY)?; + // Encode the initialize call data for BlockMeta + let block_metadata_initialize = BlockMeta::initializeCall { + _initialOwner: initial_owner, + _gasCap: gas_cap, + } + .abi_encode(); + // Encode the ERC1967Proxy constructor args + let proxy_args = ERC1967Proxy::constructorCall { + _impl: block_metadata_impl_address, + _data: block_metadata_initialize.into(), + } + .abi_encode(); + // Concatenate bytecode + constructor args for deployment + let proxy_txn_data = [proxy_impl_data, proxy_args].concat(); + let block_metadata_proxy_txn = GenesisTransaction::create( + self.address, + proxy_txn_data, + self.nonce, + block_metadata_proxy_address, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + ( + GenesisTransactionTags::BlockMetadataImpl, + block_metadata_impl_txn, + ), + ( + GenesisTransactionTags::BlockMetadata, + block_metadata_proxy_txn, + ), + ])) + } + + /// Generates genesis transactions for automation contracts deployment. + /// Deployment order follows GenesisTransactionTags: + /// + /// 1. DiamondCutFacet, // Facet contract enabling APIs to extend facets and executed registered facet + /// 2. DiamondLoupeFacet, // Facet providing API to query registered facets + /// 3. OwnershipFacet, // Facet to manage ownership credential updates and checks + /// 4. ConfigFacet, // Facet to manage automation registry configuration + /// 5. RegistryFacet, // Facet providing API for task registration, cancellation and registry state query + /// 6. CoreFacet, // Facet providing API to monitor cycle and initiate bookkeeping on cycle transition + /// 7. DiamondInit, + /// 8. Diamond, // The wrapper contract of all the facets, main entry point of automation registry API + /// All contracts addresses are pre-computed before deployment to handle circular dependencies if any. + fn setup_automation_registry( + &mut self, + owner: Address, + erc20_supra_address: Address, + registry_config: AutomationRegistryConfig, + ) -> Result> { + let config = registry_config + .v1() + .ok_or_else(|| anyhow!("Unhandled configuration version"))?; + // Pre-compute all deployment addresses + // nonce+0: DiamondCutFacet (implementation for Diamond proxy) + // nonce+1: DiamondLoupeFacet + // nonce+2: OwnershipFacet + // nonce+3: ConfigFacet + // nonce+4: RegistryFacet + // nonce+5: CoreFacet + // nonce+6: DiamondInit + // nonce+7: Diamond (proxy to all facets APIs) + + let diamond_cut_facet_addr = self.address.create(self.nonce); + let diamond_loupe_facet_addr = self.address.create(self.nonce + 1); + let ownership_facet_addr = self.address.create(self.nonce + 2); + let config_facet_addr = self.address.create(self.nonce + 3); + let registry_facet_addr = self.address.create(self.nonce + 4); + let core_facet_addr = self.address.create(self.nonce + 5); + let diamond_init_addr = self.address.create(self.nonce + 6); + let diamond_addr = self.address.create(self.nonce + 7); + + // ------------------------------------------------------------------------- + // 1. Deploy DiamondCutFacet (no constructor args) + // ------------------------------------------------------------------------- + let diamond_cut = Self::load_contract_bytecode(DIAMOND_CUT_FACET)?; + let diamond_cut_txn = GenesisTransaction::create( + self.address, + diamond_cut, + self.nonce, + diamond_cut_facet_addr, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 2. Deploy DiamondLoupeFacet + // ------------------------------------------------------------------------- + let diamond_loupe_data = Self::load_contract_bytecode(DIAMOND_LOUPE_FACET)?; + let diamond_loupe_txn = GenesisTransaction::create( + self.address, + diamond_loupe_data, + self.nonce, + diamond_loupe_facet_addr, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 3. Deploy Ownership facet + // ------------------------------------------------------------------------- + let ownership_data = Self::load_contract_bytecode(OWNERSHIP_FACET)?; + let ownership_txn = GenesisTransaction::create( + self.address, + ownership_data, + self.nonce, + ownership_facet_addr, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 4. Deploy Config Facet + // ------------------------------------------------------------------------- + let config_data = Self::load_contract_bytecode(CONFIG_FACET)?; + let config_txn = + GenesisTransaction::create(self.address, config_data, self.nonce, config_facet_addr); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 5. Deploy Register facet + // ------------------------------------------------------------------------- + let register_data = Self::load_contract_bytecode(REGISTRY_FACET)?; + let register_txn = GenesisTransaction::create( + self.address, + register_data, + self.nonce, + registry_facet_addr, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 6. Deploy Core facet + // ------------------------------------------------------------------------- + let core_data = Self::load_contract_bytecode(CORE_FACET)?; + let core_txn = + GenesisTransaction::create(self.address, core_data, self.nonce, core_facet_addr); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 7. Deploy Core facet + // ------------------------------------------------------------------------- + let diamond_init_data = Self::load_contract_bytecode(DIAMOND_INIT)?; + let diamond_init_txn = GenesisTransaction::create( + self.address, + diamond_init_data, + self.nonce, + diamond_init_addr, + ); + self.nonce += 1; + + // ------------------------------------------------------------------------- + // 7. Deploy Diamond + // ------------------------------------------------------------------------- + let diamond_init_data = Self::load_contract_bytecode(DIAMOND)?; + let facets = FacetsDeployment { + diamondCutFacet: diamond_cut_facet_addr, + loupeFacet: diamond_loupe_facet_addr, + ownershipFacet: ownership_facet_addr, + configFacet: config_facet_addr, + registryFacet: registry_facet_addr, + coreFacet: core_facet_addr, + diamondInit: diamond_init_addr, + }; + + let init_params = InitParams { + taskDurationCapSecs: config.task_duration_cap_secs, + registryMaxGasCap: config.registry_max_gas_cap, + automationBaseFeeWeiPerSec: config.automation_base_fee_wei_per_sec, + flatRegistrationFeeWei: config.flat_registration_fee_wei, + congestionThresholdPercentage: config.congestion_threshold_percentage, + congestionBaseFeeWeiPerSec: config.congestion_base_fee_wei_per_sec, + congestionExponent: config.congestion_exponent, + maxCongestionExponent: config.max_congestion_exponent, + taskCapacity: config.task_capacity, + cycleDurationSecs: config.cycle_duration_secs, + sysTaskDurationCapSecs: config.sys_task_duration_cap_secs, + sysRegistryMaxGasCap: config.sys_registry_max_gas_cap, + sysTaskCapacity: config.sys_task_capacity, + registrationEnabled: true, + automationEnabled: config.enable_automation_feature, + }; + + let diamond_constructor_data = Diamond::constructorCall { + _contractOwner: owner, + _facets: facets, + _erc20Supra: erc20_supra_address, + _params: init_params, + } + .abi_encode(); + let diamond_data = [diamond_init_data, diamond_constructor_data].concat(); + let diamond_txn = + GenesisTransaction::create(self.address, diamond_data, self.nonce, diamond_addr); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::DiamondCutFacet, diamond_cut_txn), + (GenesisTransactionTags::Diamond, diamond_txn), + (GenesisTransactionTags::DiamondLoupeFacet, diamond_loupe_txn), + (GenesisTransactionTags::OwnershipFacet, ownership_txn), + (GenesisTransactionTags::ConfigFacet, config_txn), + (GenesisTransactionTags::RegistryFacet, register_txn), + (GenesisTransactionTags::CoreFacet, core_txn), + (GenesisTransactionTags::DiamondInit, diamond_init_txn), + ])) + } + + fn load_contract_bytecode(name: &str) -> Result> { + // Bytecodes are embedded at compile time via include_bytes! macros + CONTRACT_BYTECODES + .get(name) + .map(|v| v.clone()) + .ok_or_else(|| anyhow!("Failed to get bytecode for contract: {name}")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contracts::configs::AutomationRegistryConfigV1; + use primitives::supra_constants::u64_to_address; + use primitives::TxKind; + + #[test] + fn check_multisig_setup() { + let mut generator = GenesisTransactionGenerator::default(); + let owners = vec![u64_to_address(1), u64_to_address(2), u64_to_address(3)]; + let mut config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: false, + automation_config: None, + block_prologue_gas_cap: 100000, + }; + let result = generator + .prepare_genesis_transactions(config.clone()) + .unwrap(); + assert_eq!(result.len(), 8); + assert!(result.contains_key(&GenesisTransactionTags::Create2Factory)); + assert!(result.contains_key(&GenesisTransactionTags::MultisigWalletImpl)); + assert!(result.contains_key(&GenesisTransactionTags::MultisigBeacon)); + assert!(result.contains_key(&GenesisTransactionTags::FoundationWallet)); + + // The canonical EVM singleton predeploys are unconditional, independent of `full_set`. + assert!(result.contains_key(&GenesisTransactionTags::Multicall3)); + assert!(result.contains_key(&GenesisTransactionTags::SingletonFactory)); + assert!(result.contains_key(&GenesisTransactionTags::CreateX)); + assert!(result.contains_key(&GenesisTransactionTags::Erc1820Registry)); + + // Enable full set of contract generation without automation config + config.full_set = true; + let result = generator + .prepare_genesis_transactions(config) + .expect("Successful txn generation"); + assert!(result.contains_key(&GenesisTransactionTags::Create2Factory)); + assert!(result.contains_key(&GenesisTransactionTags::FoundationWallet)); + assert!(result.contains_key(&GenesisTransactionTags::BlockMetadata)); + assert!(result.contains_key(&GenesisTransactionTags::Erc20SupraImpl)); + assert!(result.contains_key(&GenesisTransactionTags::Erc20Supra)); + assert!(result.contains_key(&GenesisTransactionTags::Erc20SupraHandlerImpl)); + assert!(result.contains_key(&GenesisTransactionTags::Erc20SupraHandler)); + let erc20_supra_handler = result + .get(&GenesisTransactionTags::Erc20SupraHandler) + .unwrap(); + assert_eq!(erc20_supra_handler.value(), &0); + + // Verify automation contracts are not deployed + assert!(!result.contains_key(&GenesisTransactionTags::DiamondCutFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::Diamond)); + assert!(!result.contains_key(&GenesisTransactionTags::DiamondLoupeFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::OwnershipFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::ConfigFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::RegistryFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::CoreFacet)); + assert!(!result.contains_key(&GenesisTransactionTags::DiamondInit)); + println!("{result:#?}"); + } + + #[test] + fn canonical_singleton_predeploys_have_expected_deploy_data() { + let mut generator = GenesisTransactionGenerator::default(); + let config = GenesisTransactionGeneratorConfig { + foundation_owners: vec![u64_to_address(1), u64_to_address(2), u64_to_address(3)], + foundation_threshold: 2, + full_set: false, + automation_config: None, + block_prologue_gas_cap: 100000, + }; + let result = generator.prepare_genesis_transactions(config).unwrap(); + + let expectations = [ + ( + GenesisTransactionTags::Multicall3, + canonical_singletons::MULTICALL3_ADDRESS, + ), + ( + GenesisTransactionTags::SingletonFactory, + canonical_singletons::SINGLETON_FACTORY_ADDRESS, + ), + ( + GenesisTransactionTags::CreateX, + canonical_singletons::CREATEX_ADDRESS, + ), + ( + GenesisTransactionTags::Erc1820Registry, + canonical_singletons::ERC1820_REGISTRY_ADDRESS, + ), + ]; + for (tag, expected_address) in expectations { + let txn = result.get(&tag).expect("predeploy transaction exists"); + assert_eq!(*txn.nonce(), 0); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(expected_address)); + } + } + + #[test] + fn check_automation_with_custom_config() { + let mut generator = GenesisTransactionGenerator::default(); + let owners = vec![u64_to_address(1), u64_to_address(2), u64_to_address(3)]; + let custom_config = AutomationRegistryConfigV1 { + task_duration_cap_secs: 7200, + registry_max_gas_cap: 20_000_000, + task_capacity: 100, + ..Default::default() + }; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: true, + automation_config: Some(custom_config.into()), + block_prologue_gas_cap: 100000, + }; + let result = generator + .prepare_genesis_transactions(config) + .expect("Successful txn generation"); + + // Verify all automation contracts are deployed + assert!(result.contains_key(&GenesisTransactionTags::DiamondCutFacet)); + assert!(result.contains_key(&GenesisTransactionTags::Diamond)); + assert!(result.contains_key(&GenesisTransactionTags::DiamondLoupeFacet)); + assert!(result.contains_key(&GenesisTransactionTags::OwnershipFacet)); + assert!(result.contains_key(&GenesisTransactionTags::ConfigFacet)); + assert!(result.contains_key(&GenesisTransactionTags::RegistryFacet)); + assert!(result.contains_key(&GenesisTransactionTags::CoreFacet)); + assert!(result.contains_key(&GenesisTransactionTags::DiamondInit)); + println!("{result:#?}"); + } + + /// Verifies `block_prologue_gas_cap` from the config is actually threaded through to + /// the `BlockMeta::initialize(owner, gasCap)` call encoded into the BlockMetadata + /// proxy's deployment data, rather than e.g. being silently dropped or hardcoded. + #[test] + fn block_metadata_init_data_encodes_configured_gas_cap() { + let mut generator = GenesisTransactionGenerator::default(); + let owners = vec![u64_to_address(1), u64_to_address(2), u64_to_address(3)]; + let owner = owners[0]; + let gas_cap = 654_321u64; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: true, + automation_config: None, + block_prologue_gas_cap: gas_cap, + }; + let result = generator + .prepare_genesis_transactions(config) + .expect("Successful txn generation"); + let block_metadata_txn = result + .get(&GenesisTransactionTags::BlockMetadata) + .expect("BlockMetadata proxy txn present"); + + // The multisig foundation wallet is the actual `_initialOwner` passed to + // `setup_block_metadata`, not `owners[0]` directly - reconstructing the exact + // owner here would duplicate multisig-address derivation, so instead assert on + // the gas cap encoding alone, which is independent of which owner was used. + let expected_gas_cap_word = { + let mut word = [0u8; 32]; + word[24..].copy_from_slice(&gas_cap.to_be_bytes()); + word + }; + assert!( + block_metadata_txn + .data() + .windows(32) + .any(|w| w == expected_gas_cap_word), + "expected the 32-byte right-aligned encoding of gas_cap ({gas_cap}) to appear \ + in the BlockMetadata proxy deployment data" + ); + + // Sanity check: a different gas cap produces different deployment data, i.e. the + // value is not a coincidental match against some unrelated fixed encoding. + let mut generator2 = GenesisTransactionGenerator::default(); + let other_gas_cap = 111_111u64; + let config2 = GenesisTransactionGeneratorConfig { + foundation_owners: vec![owner, u64_to_address(2), u64_to_address(3)], + foundation_threshold: 2, + full_set: true, + automation_config: None, + block_prologue_gas_cap: other_gas_cap, + }; + let result2 = generator2 + .prepare_genesis_transactions(config2) + .expect("Successful txn generation"); + let block_metadata_txn2 = result2 + .get(&GenesisTransactionTags::BlockMetadata) + .expect("BlockMetadata proxy txn present"); + assert_ne!(block_metadata_txn.data(), block_metadata_txn2.data()); + } + + #[test] + fn check_generator_fails_with_invalid_config() { + let mut generator = GenesisTransactionGenerator::default(); + let owners = vec![u64_to_address(1), u64_to_address(2), u64_to_address(3)]; + let invalid_config = AutomationRegistryConfigV1 { + task_duration_cap_secs: 0, // Invalid value + ..Default::default() + }; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners.clone(), + foundation_threshold: 2, + full_set: true, + automation_config: Some(invalid_config.into()), + block_prologue_gas_cap: 100000, + }; + let result = generator.prepare_genesis_transactions(config); + assert!(result.is_err(), "Expected error due to invalid config"); + + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 10, + full_set: true, + automation_config: None, + block_prologue_gas_cap: 100000, + }; + let result = generator.prepare_genesis_transactions(config); + assert!(result.is_err(), "Expected error due to invalid config"); + } +} diff --git a/crates/supra-extension/src/contracts/mod.rs b/crates/supra-extension/src/contracts/mod.rs new file mode 100644 index 0000000000..29e7e3f16a --- /dev/null +++ b/crates/supra-extension/src/contracts/mod.rs @@ -0,0 +1,6 @@ +//! Provides means to generate data for genesis contract deployment transactions + +pub mod canonical_singletons; +pub mod configs; +pub mod generator; +pub mod transaction; diff --git a/crates/supra-extension/src/contracts/transaction.rs b/crates/supra-extension/src/contracts/transaction.rs new file mode 100644 index 0000000000..b336a97147 --- /dev/null +++ b/crates/supra-extension/src/contracts/transaction.rs @@ -0,0 +1,329 @@ +//! Encloses data representing genesis contracts. + +use crate::contracts::canonical_singletons::CREATE2_FACTORY_ADDRESS; +use derive_getters::{Dissolve, Getters}; +use derive_more::Constructor; +use primitives::{keccak256, Address, TxKind}; +use serde::{Deserialize, Serialize}; +use serde_with::hex::Hex; +use serde_with::serde_as; +use std::cmp::Ordering; +use std::fmt::{Debug, Display}; + +/// Represents data required to construct genesis contracts deployment transaction +#[serde_as] +#[derive(Clone, Getters, Dissolve, Constructor, Serialize, Deserialize)] +pub struct GenesisTransaction { + /// Sender of the transaction + sender: Address, + /// Expected nonce of the sender account. + nonce: u64, + /// Amount to mint to the contract address if the transaction deploys a contract. + value: u128, + /// Input data of the transaction. + #[serde_as(as = "Hex")] + data: Vec, + /// Kind of the transaction. + kind: TxKind, + /// Pre-computed deploy address of the contract if the transaction deploys a contract. + deploy_address: Option
, +} + +impl GenesisTransaction { + /// Creates a new genesis transaction with the given parameters to deploy a contract via standard create API. + pub fn create(sender: Address, data: Vec, nonce: u64, deploy_address: Address) -> Self { + Self::new(sender, nonce, 0, data, TxKind::Create, Some(deploy_address)) + } + + /// Creates a new genesis transaction with the given parameters to deploy a contract via create2 API. + pub fn create2(sender: Address, salt: &str, data: Vec, nonce: u64) -> Self { + let salt_hash = keccak256(salt); + let deploy_address = CREATE2_FACTORY_ADDRESS.create2_from_code(salt_hash, &data.as_slice()); + let call_data = [salt_hash.to_vec(), data].concat(); + Self::new( + sender, + nonce, + 0, + call_data, + TxKind::Call(CREATE2_FACTORY_ADDRESS), + Some(deploy_address), + ) + } + + /// Creates a new genesis transaction with the given parameters to deploy a contract via create2 API. + pub fn create2_with_value( + sender: Address, + salt: &str, + data: Vec, + nonce: u64, + value: u128, + ) -> Self { + let salt_hash = keccak256(salt); + let deploy_address = CREATE2_FACTORY_ADDRESS.create2_from_code(salt_hash, &data.as_slice()); + let call_data = [salt_hash.to_vec(), data].concat(); + Self::new( + sender, + nonce, + value, + call_data, + TxKind::Call(CREATE2_FACTORY_ADDRESS), + Some(deploy_address), + ) + } + + /// Creates a new genesis call transaction with the given parameters. + pub fn call(sender: Address, target: Address, data: Vec, nonce: u64) -> Self { + Self::new(sender, nonce, 0, data, TxKind::Call(target), None) + } + + /// Creates a new genesis call transaction with the given parameters including value. + pub fn call_with_value( + sender: Address, + target: Address, + data: Vec, + nonce: u64, + value: u128, + ) -> Self { + Self::new(sender, nonce, value, data, TxKind::Call(target), None) + } +} + +impl Debug for GenesisTransaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GenesisTransaction") + .field("sender", &self.sender) + .field("kind", &self.kind) + .field("data", &self.data.len()) + .field("nonce", &self.nonce) + .field("deploy_address", &self.deploy_address) + .field("value", &self.value) + .finish() + } +} + +/// Custom contract tag to be used by upper layer to configure a custom genesis contract transactions. +#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Constructor)] +pub struct ContractCustomTag { + /// Nonce of the contract deployment. + pub nonce: u64, + /// Contract name, used as a tag + pub name: String, +} + +/// Order custom contracts by the nonce. +impl Ord for ContractCustomTag { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.nonce.cmp(&other.nonce) { + Ordering::Equal => self.name.cmp(&other.name), + r => r, + } + } +} + +impl PartialOrd for ContractCustomTag { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Display for ContractCustomTag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +/// Genesis transaction tags which also guide deployment/execution order +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum GenesisTransactionTags { + // Canonical EVM singleton predeploys: well-known third-party contracts the wider + // EVM ecosystem/tooling expects at fixed addresses. Independent of Supra's own + // system/application contracts below, and of each other, but placed first since + // that's also their actual deployment order: this enum's derived `Ord` compares + // by discriminant (the explicit `= N` value), and that `Ord` governs the + // BTreeMap iteration order genesis transactions are executed in — while serde's + // encoding of this enum keys off declaration position instead, ignoring `= N` + // entirely. The two currently coincide only because the discriminants are dense + // and assigned in the same order as the declarations. + // NOTE: the discriminants and the declaration order must be kept in lockstep. + // Renumbering the `= N` values without reordering the variants (or vice versa) + // silently changes deployment/execution order without changing serde's index, + // or changes serde's index without changing deployment order — either way a + // silent divergence. Only ever append new variants; never renumber or reorder + // existing ones once this chain has live deployments depending on either. + Create2Factory = 0, + Multicall3 = 1, + SingletonFactory = 2, + CreateX = 3, + Erc1820Registry = 4, + + // Main system and foundation contracts + MultisigWalletImpl = 5, + MultisigBeacon = 6, + FoundationWallet = 7, + Erc20SupraImpl = 8, + Erc20Supra = 9, + Erc20SupraHandlerImpl = 10, + Erc20SupraHandler = 11, + BlockMetadataImpl = 12, + BlockMetadata = 13, + + // Automation registry contracts + DiamondCutFacet = 14, + DiamondLoupeFacet = 15, + OwnershipFacet = 16, + ConfigFacet = 17, + RegistryFacet = 18, + CoreFacet = 19, + DiamondInit = 20, + Diamond = 21, + + // Custom contracts injected by application layer + Custom(ContractCustomTag), +} + +impl Display for GenesisTransactionTags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GenesisTransactionTags::Custom(custom_tag) => write!(f, "{}", custom_tag), + _ => write!(f, "{:?}", self), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use primitives::address; + + const SENDER: Address = address!("0x0000000000000000000000000000000000000001"); + const TARGET: Address = address!("0x0000000000000000000000000000000000000002"); + const DEPLOY_ADDR: Address = address!("0x0000000000000000000000000000000000000003"); + + #[test] + fn check_tag_ordering() { + let tag1 = ContractCustomTag { + nonce: 1, + name: "2test".to_string(), + }; + let tag11 = ContractCustomTag { + nonce: 1, + name: "1test".to_string(), + }; + let tag2 = ContractCustomTag { + nonce: 2, + name: "1test".to_string(), + }; + let tag2_sibling = ContractCustomTag { + nonce: 2, + name: "1test".to_string(), + }; + let tag2_diff_name = ContractCustomTag { + nonce: 2, + name: "2test".to_string(), + }; + + assert!(tag1 < tag2); + assert!(tag11 < tag2); + assert_eq!(tag2, tag2_sibling); + assert_eq!(tag2.cmp(&tag2_sibling), Ordering::Equal); + assert!(tag2 < tag2_diff_name); + } + + #[test] + fn create_sets_fields_correctly() { + let data = vec![0xde, 0xad, 0xbe, 0xef]; + let nonce = 7u64; + + let txn = GenesisTransaction::create(SENDER, data.clone(), nonce, DEPLOY_ADDR); + + assert_eq!(*txn.sender(), SENDER); + assert_eq!(*txn.nonce(), nonce); + assert_eq!(*txn.data(), data); + // Plain create carries no value. + assert_eq!(*txn.value(), 0u128); + assert_eq!(*txn.kind(), TxKind::Create); + assert_eq!(*txn.deploy_address(), Some(DEPLOY_ADDR)); + } + + #[test] + fn call_sets_fields_correctly() { + let data = vec![0xca, 0xfe, 0xba, 0xbe]; + let nonce = 3u64; + + let txn = GenesisTransaction::call(SENDER, TARGET, data.clone(), nonce); + + assert_eq!(*txn.sender(), SENDER); + assert_eq!(*txn.nonce(), nonce); + assert_eq!(*txn.data(), data); + // Plain call carries no value. + assert_eq!(*txn.value(), 0u128); + assert_eq!(*txn.kind(), TxKind::Call(TARGET)); + // Call transactions have no pre-computed deploy address. + assert_eq!(*txn.deploy_address(), None); + } + + #[test] + fn call_with_value_sets_fields_correctly() { + let data = vec![0x01, 0x02]; + let nonce = 5u64; + let value = 1_000_000u128; + + let txn = GenesisTransaction::call_with_value(SENDER, TARGET, data.clone(), nonce, value); + + assert_eq!(*txn.sender(), SENDER); + assert_eq!(*txn.nonce(), nonce); + assert_eq!(*txn.data(), data); + assert_eq!(*txn.value(), value); + assert_eq!(*txn.kind(), TxKind::Call(TARGET)); + assert_eq!(*txn.deploy_address(), None); + } + + #[test] + fn create2_sets_fields_correctly() { + let salt = "my_salt"; + let bytecode = vec![0x60, 0x00, 0x60, 0x00]; + let nonce = 1u64; + + let txn = GenesisTransaction::create2(SENDER, salt, bytecode.clone(), nonce); + + // create2 wraps the call to the CREATE2 factory, so kind must target it. + assert_eq!(*txn.sender(), SENDER); + assert_eq!(*txn.nonce(), nonce); + assert_eq!(*txn.value(), 0u128); + assert_eq!(*txn.kind(), TxKind::Call(CREATE2_FACTORY_ADDRESS)); + + // The factory call-data is salt_hash ++ bytecode. + let salt_hash = keccak256(salt); + let expected_data = [salt_hash.to_vec(), bytecode.clone()].concat(); + assert_eq!(*txn.data(), expected_data); + + // The deploy address is deterministically derived from the factory address, salt, and code. + let expected_deploy = CREATE2_FACTORY_ADDRESS.create2_from_code(salt_hash, &bytecode); + assert_eq!(*txn.deploy_address(), Some(expected_deploy)); + } + + #[test] + fn create2_with_value_sets_fields_correctly() { + let salt = "salted_contract"; + let bytecode = vec![0xAB, 0xCD]; + let nonce = 2u64; + let value = 42u128; + + let txn = + GenesisTransaction::create2_with_value(SENDER, salt, bytecode.clone(), nonce, value); + + assert_eq!(*txn.sender(), SENDER); + assert_eq!(*txn.nonce(), nonce); + assert_eq!(*txn.value(), value); + assert_eq!(*txn.kind(), TxKind::Call(CREATE2_FACTORY_ADDRESS)); + + let salt_hash = keccak256(salt); + let expected_data = [salt_hash.to_vec(), bytecode.clone()].concat(); + assert_eq!(*txn.data(), expected_data); + + let expected_deploy = CREATE2_FACTORY_ADDRESS.create2_from_code(salt_hash, &bytecode); + assert_eq!(*txn.deploy_address(), Some(expected_deploy)); + } +} diff --git a/crates/supra-extension/src/errors.rs b/crates/supra-extension/src/errors.rs new file mode 100644 index 0000000000..c9c9fbbdf9 --- /dev/null +++ b/crates/supra-extension/src/errors.rs @@ -0,0 +1,77 @@ +//! Errors reported in scope of supra-extension module. + +use thiserror::Error; + +/// Supra-extension error. +#[derive(Error, Debug)] +pub enum SupraExtensionError { + /// Reported when transaction builder misses mandatory value to build final transaction. + #[error("Missing mandatory value: {0}::{1}")] + MissingBuilderValue(String, String), + + /// Reported on failure of automation task inner payload decode. + #[error("Failed to decode task payload: {error}")] + PayloadDecode { + /// Error description + #[source] + error: alloy_sol_types::Error, + }, + + /// Reported on failure of automation task predicate decode. + #[error("Failed to decode task predicate: {error}")] + PredicateDecode { + /// Error description + #[source] + error: alloy_sol_types::Error, + }, + + /// Reported on failure of automation record remove action decode. + #[error("Failed to decode automation record remove action: {error}")] + RecordRemoveDecode { + /// Error description + #[source] + error: alloy_sol_types::Error, + }, + + /// Reported on failure of automation record process action decode. + #[error("Failed to decode automation record process action: {error}")] + RecordProcessDecode { + /// Error description + #[source] + error: alloy_sol_types::Error, + }, + + /// Reported on failure of task state conversion to counterpart in native layer. + #[error( + "Invalid automation task state value: {0}, expected [0(PENDING), 1(ACTIVE), 2(CANCELLED)]" + )] + InvalidAutomationTaskStateValue(u8), + + /// Reported on failure of task state conversion to counterpart in native layer. + #[error("Invalid automation task type value: {0}, expected [0(UST), 1(GST)]")] + InvalidAutomationTaskTypeValue(u8), + + /// Reported when automated transaction builder is attempted to be built for inactive task. + #[error("Attempt to create automated transaction builder for non-active task")] + InvalidAutomationTaskStateForBuilder, + + /// Reported when automation record action can not be decoded from input bytes of the corresponding transaction + #[error("Failed to decode automation record action: {0}")] + InvalidAutomationRecord(String), +} + +/// Extracts value of the optional value or reports [`SupraExtensionError::MissingBuilderValue`]. +#[macro_export] +macro_rules! value_or_error { + ($tpy:ty, $name:literal, $value:expr) => { + match $value { + Some(v) => v, + None => { + return Err($crate::errors::SupraExtensionError::MissingBuilderValue( + std::any::type_name::<$tpy>().to_string(), + $name.to_string(), + )); + } + } + }; +} diff --git a/crates/supra-extension/src/lib.rs b/crates/supra-extension/src/lib.rs new file mode 100644 index 0000000000..963a094521 --- /dev/null +++ b/crates/supra-extension/src/lib.rs @@ -0,0 +1,15 @@ +//! # revm-supra-extension +//! Supra extensions of the transactions to support automation feature and block based checks + +#[cfg(feature = "build-utils")] +pub mod build_utils; +pub mod contracts; +pub mod errors; +#[allow(missing_docs, missing_debug_implementations)] +#[allow(elided_lifetimes_in_paths)] +mod supra_contract_bindings; +pub mod transactions; +pub use crate::supra_contract_bindings::supra_contracts_bindings::{ + LibCommon::{CycleDetails, CycleState, TaskState, TaskType}, + SupraContractsBindings::*, +}; diff --git a/crates/supra-extension/src/supra_contract_bindings/mod.rs b/crates/supra-extension/src/supra_contract_bindings/mod.rs new file mode 100644 index 0000000000..f23401cf42 --- /dev/null +++ b/crates/supra-extension/src/supra_contract_bindings/mod.rs @@ -0,0 +1,6 @@ +#![allow(unused_imports, unused_attributes, clippy::all, rustdoc::all, dead_code, unreachable_pub)] +//! This module contains the sol! generated bindings for solidity contracts. +//! This is autogenerated code. +//! Do not manually edit these files. +//! These files may be overwritten by the codegen system at any time. +pub mod r#supra_contracts_bindings; diff --git a/crates/supra-extension/src/supra_contract_bindings/supra_contracts_bindings.rs b/crates/supra-extension/src/supra_contract_bindings/supra_contracts_bindings.rs new file mode 100644 index 0000000000..846a21adaf --- /dev/null +++ b/crates/supra-extension/src/supra_contract_bindings/supra_contracts_bindings.rs @@ -0,0 +1,4676 @@ +///Module containing a contract's types and functions. +/** + +```solidity +library LibCommon { + type CycleState is uint8; + type TaskState is uint8; + type TaskType is uint8; + struct CycleDetails { uint64 index; uint64 startTime; uint64 durationSecs; CycleState state; uint64 nextTaskIndexPosition; uint64[] expectedTasksToBeProcessed; } +} +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod LibCommon { + use super::*; + use alloy::sol_types as alloy_sol_types; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct CycleState(u8); + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[automatically_derived] + impl alloy_sol_types::private::SolTypeValue for u8 { + #[inline] + fn stv_to_tokens( + &self, + ) -> as alloy_sol_types::SolType>::Token<'_> { + alloy_sol_types::private::SolTypeValue::< + alloy::sol_types::sol_data::Uint<8>, + >::stv_to_tokens(self) + } + #[inline] + fn stv_eip712_data_word(&self) -> alloy_sol_types::Word { + as alloy_sol_types::SolType>::tokenize(self) + .0 + } + #[inline] + fn stv_abi_encode_packed_to( + &self, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::SolType>::abi_encode_packed_to(self, out) + } + #[inline] + fn stv_abi_packed_encoded_size(&self) -> usize { + as alloy_sol_types::SolType>::abi_encoded_size(self) + } + } + impl CycleState { + /// The Solidity type name. + pub const NAME: &'static str = stringify!(@ name); + /// Convert from the underlying value type. + #[inline] + pub const fn from_underlying(value: u8) -> Self { + Self(value) + } + /// Return the underlying value. + #[inline] + pub const fn into_underlying(self) -> u8 { + self.0 + } + /// Return the single encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode(&self) -> alloy_sol_types::private::Vec { + ::abi_encode(&self.0) + } + /// Return the packed encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode_packed(&self) -> alloy_sol_types::private::Vec { + ::abi_encode_packed(&self.0) + } + } + #[automatically_derived] + impl From for CycleState { + fn from(value: u8) -> Self { + Self::from_underlying(value) + } + } + #[automatically_derived] + impl From for u8 { + fn from(value: CycleState) -> Self { + value.into_underlying() + } + } + #[automatically_derived] + impl alloy_sol_types::SolType for CycleState { + type RustType = u8; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SOL_NAME: &'static str = Self::NAME; + const ENCODED_SIZE: Option = as alloy_sol_types::SolType>::ENCODED_SIZE; + const PACKED_ENCODED_SIZE: Option = as alloy_sol_types::SolType>::PACKED_ENCODED_SIZE; + #[inline] + fn valid_token(token: &Self::Token<'_>) -> bool { + Self::type_check(token).is_ok() + } + #[inline] + fn type_check(token: &Self::Token<'_>) -> alloy_sol_types::Result<()> { + as alloy_sol_types::SolType>::type_check(token) + } + #[inline] + fn detokenize(token: Self::Token<'_>) -> Self::RustType { + as alloy_sol_types::SolType>::detokenize(token) + } + } + #[automatically_derived] + impl alloy_sol_types::EventTopic for CycleState { + #[inline] + fn topic_preimage_length(rust: &Self::RustType) -> usize { + as alloy_sol_types::EventTopic>::topic_preimage_length(rust) + } + #[inline] + fn encode_topic_preimage( + rust: &Self::RustType, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::EventTopic>::encode_topic_preimage(rust, out) + } + #[inline] + fn encode_topic( + rust: &Self::RustType, + ) -> alloy_sol_types::abi::token::WordToken { + as alloy_sol_types::EventTopic>::encode_topic(rust) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct TaskState(u8); + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[automatically_derived] + impl alloy_sol_types::private::SolTypeValue for u8 { + #[inline] + fn stv_to_tokens( + &self, + ) -> as alloy_sol_types::SolType>::Token<'_> { + alloy_sol_types::private::SolTypeValue::< + alloy::sol_types::sol_data::Uint<8>, + >::stv_to_tokens(self) + } + #[inline] + fn stv_eip712_data_word(&self) -> alloy_sol_types::Word { + as alloy_sol_types::SolType>::tokenize(self) + .0 + } + #[inline] + fn stv_abi_encode_packed_to( + &self, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::SolType>::abi_encode_packed_to(self, out) + } + #[inline] + fn stv_abi_packed_encoded_size(&self) -> usize { + as alloy_sol_types::SolType>::abi_encoded_size(self) + } + } + impl TaskState { + /// The Solidity type name. + pub const NAME: &'static str = stringify!(@ name); + /// Convert from the underlying value type. + #[inline] + pub const fn from_underlying(value: u8) -> Self { + Self(value) + } + /// Return the underlying value. + #[inline] + pub const fn into_underlying(self) -> u8 { + self.0 + } + /// Return the single encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode(&self) -> alloy_sol_types::private::Vec { + ::abi_encode(&self.0) + } + /// Return the packed encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode_packed(&self) -> alloy_sol_types::private::Vec { + ::abi_encode_packed(&self.0) + } + } + #[automatically_derived] + impl From for TaskState { + fn from(value: u8) -> Self { + Self::from_underlying(value) + } + } + #[automatically_derived] + impl From for u8 { + fn from(value: TaskState) -> Self { + value.into_underlying() + } + } + #[automatically_derived] + impl alloy_sol_types::SolType for TaskState { + type RustType = u8; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SOL_NAME: &'static str = Self::NAME; + const ENCODED_SIZE: Option = as alloy_sol_types::SolType>::ENCODED_SIZE; + const PACKED_ENCODED_SIZE: Option = as alloy_sol_types::SolType>::PACKED_ENCODED_SIZE; + #[inline] + fn valid_token(token: &Self::Token<'_>) -> bool { + Self::type_check(token).is_ok() + } + #[inline] + fn type_check(token: &Self::Token<'_>) -> alloy_sol_types::Result<()> { + as alloy_sol_types::SolType>::type_check(token) + } + #[inline] + fn detokenize(token: Self::Token<'_>) -> Self::RustType { + as alloy_sol_types::SolType>::detokenize(token) + } + } + #[automatically_derived] + impl alloy_sol_types::EventTopic for TaskState { + #[inline] + fn topic_preimage_length(rust: &Self::RustType) -> usize { + as alloy_sol_types::EventTopic>::topic_preimage_length(rust) + } + #[inline] + fn encode_topic_preimage( + rust: &Self::RustType, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::EventTopic>::encode_topic_preimage(rust, out) + } + #[inline] + fn encode_topic( + rust: &Self::RustType, + ) -> alloy_sol_types::abi::token::WordToken { + as alloy_sol_types::EventTopic>::encode_topic(rust) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct TaskType(u8); + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[automatically_derived] + impl alloy_sol_types::private::SolTypeValue for u8 { + #[inline] + fn stv_to_tokens( + &self, + ) -> as alloy_sol_types::SolType>::Token<'_> { + alloy_sol_types::private::SolTypeValue::< + alloy::sol_types::sol_data::Uint<8>, + >::stv_to_tokens(self) + } + #[inline] + fn stv_eip712_data_word(&self) -> alloy_sol_types::Word { + as alloy_sol_types::SolType>::tokenize(self) + .0 + } + #[inline] + fn stv_abi_encode_packed_to( + &self, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::SolType>::abi_encode_packed_to(self, out) + } + #[inline] + fn stv_abi_packed_encoded_size(&self) -> usize { + as alloy_sol_types::SolType>::abi_encoded_size(self) + } + } + impl TaskType { + /// The Solidity type name. + pub const NAME: &'static str = stringify!(@ name); + /// Convert from the underlying value type. + #[inline] + pub const fn from_underlying(value: u8) -> Self { + Self(value) + } + /// Return the underlying value. + #[inline] + pub const fn into_underlying(self) -> u8 { + self.0 + } + /// Return the single encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode(&self) -> alloy_sol_types::private::Vec { + ::abi_encode(&self.0) + } + /// Return the packed encoding of this value, delegating to the + /// underlying type. + #[inline] + pub fn abi_encode_packed(&self) -> alloy_sol_types::private::Vec { + ::abi_encode_packed(&self.0) + } + } + #[automatically_derived] + impl From for TaskType { + fn from(value: u8) -> Self { + Self::from_underlying(value) + } + } + #[automatically_derived] + impl From for u8 { + fn from(value: TaskType) -> Self { + value.into_underlying() + } + } + #[automatically_derived] + impl alloy_sol_types::SolType for TaskType { + type RustType = u8; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SOL_NAME: &'static str = Self::NAME; + const ENCODED_SIZE: Option = as alloy_sol_types::SolType>::ENCODED_SIZE; + const PACKED_ENCODED_SIZE: Option = as alloy_sol_types::SolType>::PACKED_ENCODED_SIZE; + #[inline] + fn valid_token(token: &Self::Token<'_>) -> bool { + Self::type_check(token).is_ok() + } + #[inline] + fn type_check(token: &Self::Token<'_>) -> alloy_sol_types::Result<()> { + as alloy_sol_types::SolType>::type_check(token) + } + #[inline] + fn detokenize(token: Self::Token<'_>) -> Self::RustType { + as alloy_sol_types::SolType>::detokenize(token) + } + } + #[automatically_derived] + impl alloy_sol_types::EventTopic for TaskType { + #[inline] + fn topic_preimage_length(rust: &Self::RustType) -> usize { + as alloy_sol_types::EventTopic>::topic_preimage_length(rust) + } + #[inline] + fn encode_topic_preimage( + rust: &Self::RustType, + out: &mut alloy_sol_types::private::Vec, + ) { + as alloy_sol_types::EventTopic>::encode_topic_preimage(rust, out) + } + #[inline] + fn encode_topic( + rust: &Self::RustType, + ) -> alloy_sol_types::abi::token::WordToken { + as alloy_sol_types::EventTopic>::encode_topic(rust) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**```solidity +struct CycleDetails { uint64 index; uint64 startTime; uint64 durationSecs; CycleState state; uint64 nextTaskIndexPosition; uint64[] expectedTasksToBeProcessed; } +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct CycleDetails { + #[allow(missing_docs)] + pub index: u64, + #[allow(missing_docs)] + pub startTime: u64, + #[allow(missing_docs)] + pub durationSecs: u64, + #[allow(missing_docs)] + pub state: ::RustType, + #[allow(missing_docs)] + pub nextTaskIndexPosition: u64, + #[allow(missing_docs)] + pub expectedTasksToBeProcessed: alloy::sol_types::private::Vec, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + CycleState, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Array>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + u64, + u64, + u64, + ::RustType, + u64, + alloy::sol_types::private::Vec, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: CycleDetails) -> Self { + ( + value.index, + value.startTime, + value.durationSecs, + value.state, + value.nextTaskIndexPosition, + value.expectedTasksToBeProcessed, + ) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for CycleDetails { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + index: tuple.0, + startTime: tuple.1, + durationSecs: tuple.2, + state: tuple.3, + nextTaskIndexPosition: tuple.4, + expectedTasksToBeProcessed: tuple.5, + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolValue for CycleDetails { + type SolType = Self; + } + #[automatically_derived] + impl alloy_sol_types::private::SolTypeValue for CycleDetails { + #[inline] + fn stv_to_tokens(&self) -> ::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self.index), + as alloy_sol_types::SolType>::tokenize(&self.startTime), + as alloy_sol_types::SolType>::tokenize(&self.durationSecs), + ::tokenize(&self.state), + as alloy_sol_types::SolType>::tokenize( + &self.nextTaskIndexPosition, + ), + , + > as alloy_sol_types::SolType>::tokenize( + &self.expectedTasksToBeProcessed, + ), + ) + } + #[inline] + fn stv_abi_encoded_size(&self) -> usize { + if let Some(size) = ::ENCODED_SIZE { + return size; + } + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_encoded_size(&tuple) + } + #[inline] + fn stv_eip712_data_word(&self) -> alloy_sol_types::Word { + ::eip712_hash_struct(self) + } + #[inline] + fn stv_abi_encode_packed_to( + &self, + out: &mut alloy_sol_types::private::Vec, + ) { + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_encode_packed_to(&tuple, out) + } + #[inline] + fn stv_abi_packed_encoded_size(&self) -> usize { + if let Some(size) = ::PACKED_ENCODED_SIZE { + return size; + } + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_packed_encoded_size(&tuple) + } + } + #[automatically_derived] + impl alloy_sol_types::SolType for CycleDetails { + type RustType = Self; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SOL_NAME: &'static str = ::NAME; + const ENCODED_SIZE: Option = as alloy_sol_types::SolType>::ENCODED_SIZE; + const PACKED_ENCODED_SIZE: Option = as alloy_sol_types::SolType>::PACKED_ENCODED_SIZE; + #[inline] + fn valid_token(token: &Self::Token<'_>) -> bool { + as alloy_sol_types::SolType>::valid_token(token) + } + #[inline] + fn detokenize(token: Self::Token<'_>) -> Self::RustType { + let tuple = as alloy_sol_types::SolType>::detokenize(token); + >>::from(tuple) + } + } + #[automatically_derived] + impl alloy_sol_types::SolStruct for CycleDetails { + const NAME: &'static str = "CycleDetails"; + #[inline] + fn eip712_root_type() -> alloy_sol_types::private::Cow<'static, str> { + alloy_sol_types::private::Cow::Borrowed( + "CycleDetails(uint64 index,uint64 startTime,uint64 durationSecs,uint8 state,uint64 nextTaskIndexPosition,uint64[] expectedTasksToBeProcessed)", + ) + } + #[inline] + fn eip712_components() -> alloy_sol_types::private::Vec< + alloy_sol_types::private::Cow<'static, str>, + > { + alloy_sol_types::private::Vec::new() + } + #[inline] + fn eip712_encode_type() -> alloy_sol_types::private::Cow<'static, str> { + ::eip712_root_type() + } + #[inline] + fn eip712_encode_data(&self) -> alloy_sol_types::private::Vec { + [ + as alloy_sol_types::SolType>::eip712_data_word(&self.index) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.startTime) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.durationSecs) + .0, + ::eip712_data_word( + &self.state, + ) + .0, + as alloy_sol_types::SolType>::eip712_data_word( + &self.nextTaskIndexPosition, + ) + .0, + , + > as alloy_sol_types::SolType>::eip712_data_word( + &self.expectedTasksToBeProcessed, + ) + .0, + ] + .concat() + } + } + #[automatically_derived] + impl alloy_sol_types::EventTopic for CycleDetails { + #[inline] + fn topic_preimage_length(rust: &Self::RustType) -> usize { + 0usize + + as alloy_sol_types::EventTopic>::topic_preimage_length(&rust.index) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.startTime, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.durationSecs, + ) + + ::topic_preimage_length( + &rust.state, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.nextTaskIndexPosition, + ) + + , + > as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.expectedTasksToBeProcessed, + ) + } + #[inline] + fn encode_topic_preimage( + rust: &Self::RustType, + out: &mut alloy_sol_types::private::Vec, + ) { + out.reserve( + ::topic_preimage_length(rust), + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.index, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.startTime, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.durationSecs, + out, + ); + ::encode_topic_preimage( + &rust.state, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.nextTaskIndexPosition, + out, + ); + , + > as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.expectedTasksToBeProcessed, + out, + ); + } + #[inline] + fn encode_topic( + rust: &Self::RustType, + ) -> alloy_sol_types::abi::token::WordToken { + let mut out = alloy_sol_types::private::Vec::new(); + ::encode_topic_preimage( + rust, + &mut out, + ); + alloy_sol_types::abi::token::WordToken( + alloy_sol_types::private::keccak256(out), + ) + } + } + }; + use alloy::contract as alloy_contract; + /**Creates a new wrapper around an on-chain [`LibCommon`](self) contract instance. + +See the [wrapper's documentation](`LibCommonInstance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> LibCommonInstance { + LibCommonInstance::::new(address, __provider) + } + /**A [`LibCommon`](self) instance. + +Contains type-safe methods for interacting with an on-chain instance of the +[`LibCommon`](self) contract located at a given `address`, using a given +provider `P`. + +If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) +documentation on how to provide it), the `deploy` and `deploy_builder` methods can +be used to deploy a new instance of the contract. + +See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct LibCommonInstance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for LibCommonInstance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("LibCommonInstance").field(&self.address).finish() + } + } + /// Instantiation and getters/setters. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > LibCommonInstance { + /**Creates a new wrapper around an on-chain [`LibCommon`](self) contract instance. + +See the [wrapper's documentation](`LibCommonInstance`) for more details.*/ + #[inline] + pub const fn new( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl LibCommonInstance<&P, N> { + /// Clones the provider and returns a new instance with the cloned provider. + #[inline] + pub fn with_cloned_provider(self) -> LibCommonInstance { + LibCommonInstance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > LibCommonInstance { + /// Creates a new call builder using this contract instance's provider and address. + /// + /// Note that the call can be any function call, not just those defined in this + /// contract. Prefer using the other methods for building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + } + /// Event filters. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > LibCommonInstance { + /// Creates a new event filter using this contract instance's provider and address. + /// + /// Note that the type can be any event, not just those defined in this contract. + /// Prefer using the other methods for building type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} +/** + +Generated by the following Solidity interface... +```solidity +library LibCommon { + type CycleState is uint8; + type TaskState is uint8; + type TaskType is uint8; + struct CycleDetails { + uint64 index; + uint64 startTime; + uint64 durationSecs; + CycleState state; + uint64 nextTaskIndexPosition; + uint64[] expectedTasksToBeProcessed; + } +} + +interface SupraContractsBindings { + struct TaskMetadata { + uint128 maxGasAmount; + uint128 gasPriceCap; + uint128 automationFeeCapForCycle; + uint128 depositFee; + bytes32 txHash; + uint64 taskIndex; + uint64 registrationTime; + uint64 expiryTime; + uint64 priority; + address owner; + LibCommon.TaskType taskType; + LibCommon.TaskState taskState; + bytes payloadTx; + bytes predicate; + bytes[] auxData; + } + + event AutomationCycleEvent(uint64 indexed index, LibCommon.CycleState indexed state, uint64 startTime, uint64 durationSecs, LibCommon.CycleState indexed oldState); + + function blockPrologue() external; + function getActiveTaskIds() external view returns (uint256[] memory); + function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory); + function getTaskDetails(uint64 _taskIndex) external view returns (TaskMetadata memory); + function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (TaskMetadata[] memory); + function getTaskIdList() external view returns (uint256[] memory); + function ifTaskExists(uint64 _taskIndex) external view returns (bool); + function isAutomationReadyEnabled() external view returns (bool); + function isInitialized() external view returns (bool); + function processTasks(uint64 _cycleIndex, uint256[] memory _taskIndexes) external; + function removeRegisteredTask(uint64 _cycleIndex, uint64 _taskIndex, string memory _reason) external; +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "function", + "name": "blockPrologue", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getActiveTaskIds", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCycleStateDetails", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct LibCommon.CycleDetails", + "components": [ + { + "name": "index", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "startTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "durationSecs", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "state", + "type": "uint8", + "internalType": "enum LibCommon.CycleState" + }, + { + "name": "nextTaskIndexPosition", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expectedTasksToBeProcessed", + "type": "uint64[]", + "internalType": "uint64[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTaskDetails", + "inputs": [ + { + "name": "_taskIndex", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct TaskMetadata", + "components": [ + { + "name": "maxGasAmount", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "gasPriceCap", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "automationFeeCapForCycle", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "depositFee", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "txHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "taskIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "registrationTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expiryTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "priority", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "taskType", + "type": "uint8", + "internalType": "enum LibCommon.TaskType" + }, + { + "name": "taskState", + "type": "uint8", + "internalType": "enum LibCommon.TaskState" + }, + { + "name": "payloadTx", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "predicate", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "auxData", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTaskDetailsBulk", + "inputs": [ + { + "name": "_taskIndexes", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple[]", + "internalType": "struct TaskMetadata[]", + "components": [ + { + "name": "maxGasAmount", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "gasPriceCap", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "automationFeeCapForCycle", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "depositFee", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "txHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "taskIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "registrationTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "expiryTime", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "priority", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "taskType", + "type": "uint8", + "internalType": "enum LibCommon.TaskType" + }, + { + "name": "taskState", + "type": "uint8", + "internalType": "enum LibCommon.TaskState" + }, + { + "name": "payloadTx", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "predicate", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "auxData", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTaskIdList", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ifTaskExists", + "inputs": [ + { + "name": "_taskIndex", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isAutomationReadyEnabled", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isInitialized", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "processTasks", + "inputs": [ + { + "name": "_cycleIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_taskIndexes", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeRegisteredTask", + "inputs": [ + { + "name": "_cycleIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_taskIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_reason", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AutomationCycleEvent", + "inputs": [ + { + "name": "index", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "state", + "type": "uint8", + "indexed": true, + "internalType": "enum LibCommon.CycleState" + }, + { + "name": "startTime", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "durationSecs", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "oldState", + "type": "uint8", + "indexed": true, + "internalType": "enum LibCommon.CycleState" + } + ], + "anonymous": false + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod SupraContractsBindings { + use super::*; + use alloy::sol_types as alloy_sol_types; + /// The creation / init bytecode of the contract. + /// + /// ```text + ///0x + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"", + ); + /// The runtime bytecode of the contract, as deployed on the network. + /// + /// ```text + ///0x + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"", + ); + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**```solidity +struct TaskMetadata { uint128 maxGasAmount; uint128 gasPriceCap; uint128 automationFeeCapForCycle; uint128 depositFee; bytes32 txHash; uint64 taskIndex; uint64 registrationTime; uint64 expiryTime; uint64 priority; address owner; LibCommon.TaskType taskType; LibCommon.TaskState taskState; bytes payloadTx; bytes predicate; bytes[] auxData; } +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct TaskMetadata { + #[allow(missing_docs)] + pub maxGasAmount: u128, + #[allow(missing_docs)] + pub gasPriceCap: u128, + #[allow(missing_docs)] + pub automationFeeCapForCycle: u128, + #[allow(missing_docs)] + pub depositFee: u128, + #[allow(missing_docs)] + pub txHash: alloy::sol_types::private::FixedBytes<32>, + #[allow(missing_docs)] + pub taskIndex: u64, + #[allow(missing_docs)] + pub registrationTime: u64, + #[allow(missing_docs)] + pub expiryTime: u64, + #[allow(missing_docs)] + pub priority: u64, + #[allow(missing_docs)] + pub owner: alloy::sol_types::private::Address, + #[allow(missing_docs)] + pub taskType: ::RustType, + #[allow(missing_docs)] + pub taskState: ::RustType, + #[allow(missing_docs)] + pub payloadTx: alloy::sol_types::private::Bytes, + #[allow(missing_docs)] + pub predicate: alloy::sol_types::private::Bytes, + #[allow(missing_docs)] + pub auxData: alloy::sol_types::private::Vec, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Uint<128>, + alloy::sol_types::sol_data::Uint<128>, + alloy::sol_types::sol_data::Uint<128>, + alloy::sol_types::sol_data::Uint<128>, + alloy::sol_types::sol_data::FixedBytes<32>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Address, + LibCommon::TaskType, + LibCommon::TaskState, + alloy::sol_types::sol_data::Bytes, + alloy::sol_types::sol_data::Bytes, + alloy::sol_types::sol_data::Array, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + u128, + u128, + u128, + u128, + alloy::sol_types::private::FixedBytes<32>, + u64, + u64, + u64, + u64, + alloy::sol_types::private::Address, + ::RustType, + ::RustType, + alloy::sol_types::private::Bytes, + alloy::sol_types::private::Bytes, + alloy::sol_types::private::Vec, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: TaskMetadata) -> Self { + ( + value.maxGasAmount, + value.gasPriceCap, + value.automationFeeCapForCycle, + value.depositFee, + value.txHash, + value.taskIndex, + value.registrationTime, + value.expiryTime, + value.priority, + value.owner, + value.taskType, + value.taskState, + value.payloadTx, + value.predicate, + value.auxData, + ) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for TaskMetadata { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + maxGasAmount: tuple.0, + gasPriceCap: tuple.1, + automationFeeCapForCycle: tuple.2, + depositFee: tuple.3, + txHash: tuple.4, + taskIndex: tuple.5, + registrationTime: tuple.6, + expiryTime: tuple.7, + priority: tuple.8, + owner: tuple.9, + taskType: tuple.10, + taskState: tuple.11, + payloadTx: tuple.12, + predicate: tuple.13, + auxData: tuple.14, + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolValue for TaskMetadata { + type SolType = Self; + } + #[automatically_derived] + impl alloy_sol_types::private::SolTypeValue for TaskMetadata { + #[inline] + fn stv_to_tokens(&self) -> ::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self.maxGasAmount), + as alloy_sol_types::SolType>::tokenize(&self.gasPriceCap), + as alloy_sol_types::SolType>::tokenize( + &self.automationFeeCapForCycle, + ), + as alloy_sol_types::SolType>::tokenize(&self.depositFee), + as alloy_sol_types::SolType>::tokenize(&self.txHash), + as alloy_sol_types::SolType>::tokenize(&self.taskIndex), + as alloy_sol_types::SolType>::tokenize(&self.registrationTime), + as alloy_sol_types::SolType>::tokenize(&self.expiryTime), + as alloy_sol_types::SolType>::tokenize(&self.priority), + ::tokenize( + &self.owner, + ), + ::tokenize( + &self.taskType, + ), + ::tokenize( + &self.taskState, + ), + ::tokenize( + &self.payloadTx, + ), + ::tokenize( + &self.predicate, + ), + as alloy_sol_types::SolType>::tokenize(&self.auxData), + ) + } + #[inline] + fn stv_abi_encoded_size(&self) -> usize { + if let Some(size) = ::ENCODED_SIZE { + return size; + } + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_encoded_size(&tuple) + } + #[inline] + fn stv_eip712_data_word(&self) -> alloy_sol_types::Word { + ::eip712_hash_struct(self) + } + #[inline] + fn stv_abi_encode_packed_to( + &self, + out: &mut alloy_sol_types::private::Vec, + ) { + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_encode_packed_to(&tuple, out) + } + #[inline] + fn stv_abi_packed_encoded_size(&self) -> usize { + if let Some(size) = ::PACKED_ENCODED_SIZE { + return size; + } + let tuple = as ::core::convert::From>::from(self.clone()); + as alloy_sol_types::SolType>::abi_packed_encoded_size(&tuple) + } + } + #[automatically_derived] + impl alloy_sol_types::SolType for TaskMetadata { + type RustType = Self; + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SOL_NAME: &'static str = ::NAME; + const ENCODED_SIZE: Option = as alloy_sol_types::SolType>::ENCODED_SIZE; + const PACKED_ENCODED_SIZE: Option = as alloy_sol_types::SolType>::PACKED_ENCODED_SIZE; + #[inline] + fn valid_token(token: &Self::Token<'_>) -> bool { + as alloy_sol_types::SolType>::valid_token(token) + } + #[inline] + fn detokenize(token: Self::Token<'_>) -> Self::RustType { + let tuple = as alloy_sol_types::SolType>::detokenize(token); + >>::from(tuple) + } + } + #[automatically_derived] + impl alloy_sol_types::SolStruct for TaskMetadata { + const NAME: &'static str = "TaskMetadata"; + #[inline] + fn eip712_root_type() -> alloy_sol_types::private::Cow<'static, str> { + alloy_sol_types::private::Cow::Borrowed( + "TaskMetadata(uint128 maxGasAmount,uint128 gasPriceCap,uint128 automationFeeCapForCycle,uint128 depositFee,bytes32 txHash,uint64 taskIndex,uint64 registrationTime,uint64 expiryTime,uint64 priority,address owner,uint8 taskType,uint8 taskState,bytes payloadTx,bytes predicate,bytes[] auxData)", + ) + } + #[inline] + fn eip712_components() -> alloy_sol_types::private::Vec< + alloy_sol_types::private::Cow<'static, str>, + > { + alloy_sol_types::private::Vec::new() + } + #[inline] + fn eip712_encode_type() -> alloy_sol_types::private::Cow<'static, str> { + ::eip712_root_type() + } + #[inline] + fn eip712_encode_data(&self) -> alloy_sol_types::private::Vec { + [ + as alloy_sol_types::SolType>::eip712_data_word(&self.maxGasAmount) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.gasPriceCap) + .0, + as alloy_sol_types::SolType>::eip712_data_word( + &self.automationFeeCapForCycle, + ) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.depositFee) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.txHash) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.taskIndex) + .0, + as alloy_sol_types::SolType>::eip712_data_word( + &self.registrationTime, + ) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.expiryTime) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.priority) + .0, + ::eip712_data_word( + &self.owner, + ) + .0, + ::eip712_data_word( + &self.taskType, + ) + .0, + ::eip712_data_word( + &self.taskState, + ) + .0, + ::eip712_data_word( + &self.payloadTx, + ) + .0, + ::eip712_data_word( + &self.predicate, + ) + .0, + as alloy_sol_types::SolType>::eip712_data_word(&self.auxData) + .0, + ] + .concat() + } + } + #[automatically_derived] + impl alloy_sol_types::EventTopic for TaskMetadata { + #[inline] + fn topic_preimage_length(rust: &Self::RustType) -> usize { + 0usize + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.maxGasAmount, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.gasPriceCap, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.automationFeeCapForCycle, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.depositFee, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.txHash, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.taskIndex, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.registrationTime, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.expiryTime, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.priority, + ) + + ::topic_preimage_length( + &rust.owner, + ) + + ::topic_preimage_length( + &rust.taskType, + ) + + ::topic_preimage_length( + &rust.taskState, + ) + + ::topic_preimage_length( + &rust.payloadTx, + ) + + ::topic_preimage_length( + &rust.predicate, + ) + + as alloy_sol_types::EventTopic>::topic_preimage_length( + &rust.auxData, + ) + } + #[inline] + fn encode_topic_preimage( + rust: &Self::RustType, + out: &mut alloy_sol_types::private::Vec, + ) { + out.reserve( + ::topic_preimage_length(rust), + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.maxGasAmount, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.gasPriceCap, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.automationFeeCapForCycle, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.depositFee, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.txHash, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.taskIndex, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.registrationTime, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.expiryTime, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.priority, + out, + ); + ::encode_topic_preimage( + &rust.owner, + out, + ); + ::encode_topic_preimage( + &rust.taskType, + out, + ); + ::encode_topic_preimage( + &rust.taskState, + out, + ); + ::encode_topic_preimage( + &rust.payloadTx, + out, + ); + ::encode_topic_preimage( + &rust.predicate, + out, + ); + as alloy_sol_types::EventTopic>::encode_topic_preimage( + &rust.auxData, + out, + ); + } + #[inline] + fn encode_topic( + rust: &Self::RustType, + ) -> alloy_sol_types::abi::token::WordToken { + let mut out = alloy_sol_types::private::Vec::new(); + ::encode_topic_preimage( + rust, + &mut out, + ); + alloy_sol_types::abi::token::WordToken( + alloy_sol_types::private::keccak256(out), + ) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Event with signature `AutomationCycleEvent(uint64,uint8,uint64,uint64,uint8)` and selector `0xe3a609ff9d35dde784f4ecc5c5988b3a4ad5ebeabb27e7ad22a76570128e51df`. +```solidity +event AutomationCycleEvent(uint64 indexed index, LibCommon.CycleState indexed state, uint64 startTime, uint64 durationSecs, LibCommon.CycleState indexed oldState); +```*/ + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + #[derive(Clone)] + pub struct AutomationCycleEvent { + #[allow(missing_docs)] + pub index: u64, + #[allow(missing_docs)] + pub state: ::RustType, + #[allow(missing_docs)] + pub startTime: u64, + #[allow(missing_docs)] + pub durationSecs: u64, + #[allow(missing_docs)] + pub oldState: ::RustType, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + #[automatically_derived] + impl alloy_sol_types::SolEvent for AutomationCycleEvent { + type DataTuple<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + ); + type DataToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type TopicList = ( + alloy_sol_types::sol_data::FixedBytes<32>, + alloy::sol_types::sol_data::Uint<64>, + LibCommon::CycleState, + LibCommon::CycleState, + ); + const SIGNATURE: &'static str = "AutomationCycleEvent(uint64,uint8,uint64,uint64,uint8)"; + const SIGNATURE_HASH: alloy_sol_types::private::B256 = alloy_sol_types::private::B256::new([ + 227u8, 166u8, 9u8, 255u8, 157u8, 53u8, 221u8, 231u8, 132u8, 244u8, 236u8, + 197u8, 197u8, 152u8, 139u8, 58u8, 74u8, 213u8, 235u8, 234u8, 187u8, 39u8, + 231u8, 173u8, 34u8, 167u8, 101u8, 112u8, 18u8, 142u8, 81u8, 223u8, + ]); + const ANONYMOUS: bool = false; + #[allow(unused_variables)] + #[inline] + fn new( + topics: ::RustType, + data: as alloy_sol_types::SolType>::RustType, + ) -> Self { + Self { + index: topics.1, + state: topics.2, + startTime: data.0, + durationSecs: data.1, + oldState: topics.3, + } + } + #[inline] + fn check_signature( + topics: &::RustType, + ) -> alloy_sol_types::Result<()> { + if topics.0 != Self::SIGNATURE_HASH { + return Err( + alloy_sol_types::Error::invalid_event_signature_hash( + Self::SIGNATURE, + topics.0, + Self::SIGNATURE_HASH, + ), + ); + } + Ok(()) + } + #[inline] + fn tokenize_body(&self) -> Self::DataToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self.startTime), + as alloy_sol_types::SolType>::tokenize(&self.durationSecs), + ) + } + #[inline] + fn topics(&self) -> ::RustType { + ( + Self::SIGNATURE_HASH.into(), + self.index.clone(), + self.state.clone(), + self.oldState.clone(), + ) + } + #[inline] + fn encode_topics_raw( + &self, + out: &mut [alloy_sol_types::abi::token::WordToken], + ) -> alloy_sol_types::Result<()> { + if out.len() < ::COUNT { + return Err(alloy_sol_types::Error::Overrun); + } + out[0usize] = alloy_sol_types::abi::token::WordToken( + Self::SIGNATURE_HASH, + ); + out[1usize] = as alloy_sol_types::EventTopic>::encode_topic(&self.index); + out[2usize] = ::encode_topic( + &self.state, + ); + out[3usize] = ::encode_topic( + &self.oldState, + ); + Ok(()) + } + } + #[automatically_derived] + impl alloy_sol_types::private::IntoLogData for AutomationCycleEvent { + fn to_log_data(&self) -> alloy_sol_types::private::LogData { + From::from(self) + } + fn into_log_data(self) -> alloy_sol_types::private::LogData { + From::from(&self) + } + } + #[automatically_derived] + impl From<&AutomationCycleEvent> for alloy_sol_types::private::LogData { + #[inline] + fn from(this: &AutomationCycleEvent) -> alloy_sol_types::private::LogData { + alloy_sol_types::SolEvent::encode_log_data(this) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `blockPrologue()` and selector `0x7ded091b`. +```solidity +function blockPrologue() external; +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct blockPrologueCall; + ///Container type for the return parameters of the [`blockPrologue()`](blockPrologueCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct blockPrologueReturn {} + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: blockPrologueCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for blockPrologueCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: blockPrologueReturn) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for blockPrologueReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self {} + } + } + } + impl blockPrologueReturn { + fn _tokenize( + &self, + ) -> ::ReturnToken<'_> { + () + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for blockPrologueCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = blockPrologueReturn; + type ReturnTuple<'a> = (); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "blockPrologue()"; + const SELECTOR: [u8; 4] = [125u8, 237u8, 9u8, 27u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + blockPrologueReturn::_tokenize(ret) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(Into::into) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(Into::into) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `getActiveTaskIds()` and selector `0x2321cca3`. +```solidity +function getActiveTaskIds() external view returns (uint256[] memory); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getActiveTaskIdsCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`getActiveTaskIds()`](getActiveTaskIdsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getActiveTaskIdsReturn { + #[allow(missing_docs)] + pub _0: alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getActiveTaskIdsCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getActiveTaskIdsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getActiveTaskIdsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getActiveTaskIdsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for getActiveTaskIdsCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >; + type ReturnTuple<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "getActiveTaskIds()"; + const SELECTOR: [u8; 4] = [35u8, 33u8, 204u8, 163u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + , + > as alloy_sol_types::SolType>::tokenize(ret), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: getActiveTaskIdsReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: getActiveTaskIdsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `getCycleStateDetails()` and selector `0x6b5d8c56`. +```solidity +function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getCycleStateDetailsCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive()] + ///Container type for the return parameters of the [`getCycleStateDetails()`](getCycleStateDetailsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getCycleStateDetailsReturn { + #[allow(missing_docs)] + pub _0: ::RustType, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getCycleStateDetailsCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getCycleStateDetailsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (LibCommon::CycleDetails,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + ::RustType, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getCycleStateDetailsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getCycleStateDetailsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for getCycleStateDetailsCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = ::RustType; + type ReturnTuple<'a> = (LibCommon::CycleDetails,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "getCycleStateDetails()"; + const SELECTOR: [u8; 4] = [107u8, 93u8, 140u8, 86u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: getCycleStateDetailsReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: getCycleStateDetailsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `getTaskDetails(uint64)` and selector `0xb2ef6896`. +```solidity +function getTaskDetails(uint64 _taskIndex) external view returns (TaskMetadata memory); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskDetailsCall { + #[allow(missing_docs)] + pub _taskIndex: u64, + } + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`getTaskDetails(uint64)`](getTaskDetailsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskDetailsReturn { + #[allow(missing_docs)] + pub _0: ::RustType, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Uint<64>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (u64,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: getTaskDetailsCall) -> Self { + (value._taskIndex,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for getTaskDetailsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _taskIndex: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (TaskMetadata,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + ::RustType, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getTaskDetailsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getTaskDetailsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for getTaskDetailsCall { + type Parameters<'a> = (alloy::sol_types::sol_data::Uint<64>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = ::RustType; + type ReturnTuple<'a> = (TaskMetadata,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "getTaskDetails(uint64)"; + const SELECTOR: [u8; 4] = [178u8, 239u8, 104u8, 150u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self._taskIndex), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: getTaskDetailsReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: getTaskDetailsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `getTaskDetailsBulk(uint64[])` and selector `0x12f72cf4`. +```solidity +function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (TaskMetadata[] memory); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskDetailsBulkCall { + #[allow(missing_docs)] + pub _taskIndexes: alloy::sol_types::private::Vec, + } + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`getTaskDetailsBulk(uint64[])`](getTaskDetailsBulkCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskDetailsBulkReturn { + #[allow(missing_docs)] + pub _0: alloy::sol_types::private::Vec< + ::RustType, + >, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy::sol_types::private::Vec,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getTaskDetailsBulkCall) -> Self { + (value._taskIndexes,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getTaskDetailsBulkCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _taskIndexes: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Array, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy::sol_types::private::Vec< + ::RustType, + >, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: getTaskDetailsBulkReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for getTaskDetailsBulkReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for getTaskDetailsBulkCall { + type Parameters<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = alloy::sol_types::private::Vec< + ::RustType, + >; + type ReturnTuple<'a> = (alloy::sol_types::sol_data::Array,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "getTaskDetailsBulk(uint64[])"; + const SELECTOR: [u8; 4] = [18u8, 247u8, 44u8, 244u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + , + > as alloy_sol_types::SolType>::tokenize(&self._taskIndexes), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize(ret), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: getTaskDetailsBulkReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: getTaskDetailsBulkReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `getTaskIdList()` and selector `0xec82b429`. +```solidity +function getTaskIdList() external view returns (uint256[] memory); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskIdListCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`getTaskIdList()`](getTaskIdListCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct getTaskIdListReturn { + #[allow(missing_docs)] + pub _0: alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: getTaskIdListCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for getTaskIdListCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: getTaskIdListReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for getTaskIdListReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for getTaskIdListCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >; + type ReturnTuple<'a> = ( + alloy::sol_types::sol_data::Array>, + ); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "getTaskIdList()"; + const SELECTOR: [u8; 4] = [236u8, 130u8, 180u8, 41u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + , + > as alloy_sol_types::SolType>::tokenize(ret), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: getTaskIdListReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: getTaskIdListReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `ifTaskExists(uint64)` and selector `0x8aaa404e`. +```solidity +function ifTaskExists(uint64 _taskIndex) external view returns (bool); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct ifTaskExistsCall { + #[allow(missing_docs)] + pub _taskIndex: u64, + } + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`ifTaskExists(uint64)`](ifTaskExistsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct ifTaskExistsReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Uint<64>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (u64,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: ifTaskExistsCall) -> Self { + (value._taskIndex,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for ifTaskExistsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _taskIndex: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: ifTaskExistsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for ifTaskExistsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for ifTaskExistsCall { + type Parameters<'a> = (alloy::sol_types::sol_data::Uint<64>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = bool; + type ReturnTuple<'a> = (alloy::sol_types::sol_data::Bool,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "ifTaskExists(uint64)"; + const SELECTOR: [u8; 4] = [138u8, 170u8, 64u8, 78u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self._taskIndex), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + ::tokenize( + ret, + ), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: ifTaskExistsReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: ifTaskExistsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `isAutomationReadyEnabled()` and selector `0x105176ce`. +```solidity +function isAutomationReadyEnabled() external view returns (bool); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isAutomationReadyEnabledCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`isAutomationReadyEnabled()`](isAutomationReadyEnabledCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isAutomationReadyEnabledReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: isAutomationReadyEnabledCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for isAutomationReadyEnabledCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: isAutomationReadyEnabledReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for isAutomationReadyEnabledReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for isAutomationReadyEnabledCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = bool; + type ReturnTuple<'a> = (alloy::sol_types::sol_data::Bool,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "isAutomationReadyEnabled()"; + const SELECTOR: [u8; 4] = [16u8, 81u8, 118u8, 206u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + ::tokenize( + ret, + ), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: isAutomationReadyEnabledReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: isAutomationReadyEnabledReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `isInitialized()` and selector `0x392e53cd`. +```solidity +function isInitialized() external view returns (bool); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isInitializedCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`isInitialized()`](isInitializedCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isInitializedReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: isInitializedCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for isInitializedCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: isInitializedReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for isInitializedReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for isInitializedCall { + type Parameters<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = bool; + type ReturnTuple<'a> = (alloy::sol_types::sol_data::Bool,); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "isInitialized()"; + const SELECTOR: [u8; 4] = [57u8, 46u8, 83u8, 205u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + ::tokenize( + ret, + ), + ) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(|r| { + let r: isInitializedReturn = r.into(); + r._0 + }) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(|r| { + let r: isInitializedReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `processTasks(uint64,uint256[])` and selector `0x40b7cbc6`. +```solidity +function processTasks(uint64 _cycleIndex, uint256[] memory _taskIndexes) external; +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct processTasksCall { + #[allow(missing_docs)] + pub _cycleIndex: u64, + #[allow(missing_docs)] + pub _taskIndexes: alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + } + ///Container type for the return parameters of the [`processTasks(uint64,uint256[])`](processTasksCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct processTasksReturn {} + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Array>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + u64, + alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: processTasksCall) -> Self { + (value._cycleIndex, value._taskIndexes) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for processTasksCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + _cycleIndex: tuple.0, + _taskIndexes: tuple.1, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: processTasksReturn) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for processTasksReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self {} + } + } + } + impl processTasksReturn { + fn _tokenize( + &self, + ) -> ::ReturnToken<'_> { + () + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for processTasksCall { + type Parameters<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Array>, + ); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = processTasksReturn; + type ReturnTuple<'a> = (); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "processTasks(uint64,uint256[])"; + const SELECTOR: [u8; 4] = [64u8, 183u8, 203u8, 198u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self._cycleIndex), + , + > as alloy_sol_types::SolType>::tokenize(&self._taskIndexes), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + processTasksReturn::_tokenize(ret) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(Into::into) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(Into::into) + } + } + }; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `removeRegisteredTask(uint64,uint64,string)` and selector `0x313fc5e5`. +```solidity +function removeRegisteredTask(uint64 _cycleIndex, uint64 _taskIndex, string memory _reason) external; +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct removeRegisteredTaskCall { + #[allow(missing_docs)] + pub _cycleIndex: u64, + #[allow(missing_docs)] + pub _taskIndex: u64, + #[allow(missing_docs)] + pub _reason: alloy::sol_types::private::String, + } + ///Container type for the return parameters of the [`removeRegisteredTask(uint64,uint64,string)`](removeRegisteredTaskCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct removeRegisteredTaskReturn {} + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy::sol_types as alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::String, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (u64, u64, alloy::sol_types::private::String); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: removeRegisteredTaskCall) -> Self { + (value._cycleIndex, value._taskIndex, value._reason) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for removeRegisteredTaskCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + _cycleIndex: tuple.0, + _taskIndex: tuple.1, + _reason: tuple.2, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion( + _t: alloy_sol_types::private::AssertTypeEq, + ) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From + for UnderlyingRustTuple<'_> { + fn from(value: removeRegisteredTaskReturn) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for removeRegisteredTaskReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self {} + } + } + } + impl removeRegisteredTaskReturn { + fn _tokenize( + &self, + ) -> ::ReturnToken< + '_, + > { + () + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for removeRegisteredTaskCall { + type Parameters<'a> = ( + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::Uint<64>, + alloy::sol_types::sol_data::String, + ); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + type Return = removeRegisteredTaskReturn; + type ReturnTuple<'a> = (); + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + const SIGNATURE: &'static str = "removeRegisteredTask(uint64,uint64,string)"; + const SELECTOR: [u8; 4] = [49u8, 63u8, 197u8, 229u8]; + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize(&self._cycleIndex), + as alloy_sol_types::SolType>::tokenize(&self._taskIndex), + ::tokenize( + &self._reason, + ), + ) + } + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + removeRegisteredTaskReturn::_tokenize(ret) + } + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(Into::into) + } + #[inline] + fn abi_decode_returns_validate( + data: &[u8], + ) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate(data) + .map(Into::into) + } + } + }; + ///Container for all the [`SupraContractsBindings`](self) function calls. + #[derive(Clone)] + #[derive(serde::Serialize, serde::Deserialize)] + #[derive()] + pub enum SupraContractsBindingsCalls { + #[allow(missing_docs)] + blockPrologue(blockPrologueCall), + #[allow(missing_docs)] + getActiveTaskIds(getActiveTaskIdsCall), + #[allow(missing_docs)] + getCycleStateDetails(getCycleStateDetailsCall), + #[allow(missing_docs)] + getTaskDetails(getTaskDetailsCall), + #[allow(missing_docs)] + getTaskDetailsBulk(getTaskDetailsBulkCall), + #[allow(missing_docs)] + getTaskIdList(getTaskIdListCall), + #[allow(missing_docs)] + ifTaskExists(ifTaskExistsCall), + #[allow(missing_docs)] + isAutomationReadyEnabled(isAutomationReadyEnabledCall), + #[allow(missing_docs)] + isInitialized(isInitializedCall), + #[allow(missing_docs)] + processTasks(processTasksCall), + #[allow(missing_docs)] + removeRegisteredTask(removeRegisteredTaskCall), + } + impl SupraContractsBindingsCalls { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the variants. + /// No guarantees are made about the order of the selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = &[ + [16u8, 81u8, 118u8, 206u8], + [18u8, 247u8, 44u8, 244u8], + [35u8, 33u8, 204u8, 163u8], + [49u8, 63u8, 197u8, 229u8], + [57u8, 46u8, 83u8, 205u8], + [64u8, 183u8, 203u8, 198u8], + [107u8, 93u8, 140u8, 86u8], + [125u8, 237u8, 9u8, 27u8], + [138u8, 170u8, 64u8, 78u8], + [178u8, 239u8, 104u8, 150u8], + [236u8, 130u8, 180u8, 41u8], + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::core::stringify!(isAutomationReadyEnabled), + ::core::stringify!(getTaskDetailsBulk), + ::core::stringify!(getActiveTaskIds), + ::core::stringify!(removeRegisteredTask), + ::core::stringify!(isInitialized), + ::core::stringify!(processTasks), + ::core::stringify!(getCycleStateDetails), + ::core::stringify!(blockPrologue), + ::core::stringify!(ifTaskExists), + ::core::stringify!(getTaskDetails), + ::core::stringify!(getTaskIdList), + ]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = &[ + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ]; + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for SupraContractsBindingsCalls { + const NAME: &'static str = "SupraContractsBindingsCalls"; + const MIN_DATA_LENGTH: usize = 0usize; + const COUNT: usize = 11usize; + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::blockPrologue(_) => { + ::SELECTOR + } + Self::getActiveTaskIds(_) => { + ::SELECTOR + } + Self::getCycleStateDetails(_) => { + ::SELECTOR + } + Self::getTaskDetails(_) => { + ::SELECTOR + } + Self::getTaskDetailsBulk(_) => { + ::SELECTOR + } + Self::getTaskIdList(_) => { + ::SELECTOR + } + Self::ifTaskExists(_) => { + ::SELECTOR + } + Self::isAutomationReadyEnabled(_) => { + ::SELECTOR + } + Self::isInitialized(_) => { + ::SELECTOR + } + Self::processTasks(_) => { + ::SELECTOR + } + Self::removeRegisteredTask(_) => { + ::SELECTOR + } + } + } + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn( + &[u8], + ) -> alloy_sol_types::Result] = &[ + { + fn isAutomationReadyEnabled( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::isAutomationReadyEnabled) + } + isAutomationReadyEnabled + }, + { + fn getTaskDetailsBulk( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::getTaskDetailsBulk) + } + getTaskDetailsBulk + }, + { + fn getActiveTaskIds( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::getActiveTaskIds) + } + getActiveTaskIds + }, + { + fn removeRegisteredTask( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::removeRegisteredTask) + } + removeRegisteredTask + }, + { + fn isInitialized( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::isInitialized) + } + isInitialized + }, + { + fn processTasks( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::processTasks) + } + processTasks + }, + { + fn getCycleStateDetails( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::getCycleStateDetails) + } + getCycleStateDetails + }, + { + fn blockPrologue( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::blockPrologue) + } + blockPrologue + }, + { + fn ifTaskExists( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::ifTaskExists) + } + ifTaskExists + }, + { + fn getTaskDetails( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::getTaskDetails) + } + getTaskDetails + }, + { + fn getTaskIdList( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::getTaskIdList) + } + getTaskIdList + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err( + alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + ), + ); + }; + DECODE_SHIMS[idx](data) + } + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn( + &[u8], + ) -> alloy_sol_types::Result] = &[ + { + fn isAutomationReadyEnabled( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::isAutomationReadyEnabled) + } + isAutomationReadyEnabled + }, + { + fn getTaskDetailsBulk( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::getTaskDetailsBulk) + } + getTaskDetailsBulk + }, + { + fn getActiveTaskIds( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::getActiveTaskIds) + } + getActiveTaskIds + }, + { + fn removeRegisteredTask( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::removeRegisteredTask) + } + removeRegisteredTask + }, + { + fn isInitialized( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::isInitialized) + } + isInitialized + }, + { + fn processTasks( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::processTasks) + } + processTasks + }, + { + fn getCycleStateDetails( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::getCycleStateDetails) + } + getCycleStateDetails + }, + { + fn blockPrologue( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::blockPrologue) + } + blockPrologue + }, + { + fn ifTaskExists( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::ifTaskExists) + } + ifTaskExists + }, + { + fn getTaskDetails( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::getTaskDetails) + } + getTaskDetails + }, + { + fn getTaskIdList( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::getTaskIdList) + } + getTaskIdList + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err( + alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + ), + ); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::blockPrologue(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::getActiveTaskIds(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::getCycleStateDetails(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::getTaskDetails(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::getTaskDetailsBulk(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::getTaskIdList(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::ifTaskExists(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::isAutomationReadyEnabled(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::isInitialized(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::processTasks(inner) => { + ::abi_encoded_size( + inner, + ) + } + Self::removeRegisteredTask(inner) => { + ::abi_encoded_size( + inner, + ) + } + } + } + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::blockPrologue(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::getActiveTaskIds(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::getCycleStateDetails(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::getTaskDetails(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::getTaskDetailsBulk(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::getTaskIdList(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::ifTaskExists(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::isAutomationReadyEnabled(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::isInitialized(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::processTasks(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + Self::removeRegisteredTask(inner) => { + ::abi_encode_raw( + inner, + out, + ) + } + } + } + } + ///Container for all the [`SupraContractsBindings`](self) events. + #[derive(Clone)] + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Debug, PartialEq, Eq, Hash)] + pub enum SupraContractsBindingsEvents { + #[allow(missing_docs)] + AutomationCycleEvent(AutomationCycleEvent), + } + impl SupraContractsBindingsEvents { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the variants. + /// No guarantees are made about the order of the selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 32usize]] = &[ + [ + 227u8, 166u8, 9u8, 255u8, 157u8, 53u8, 221u8, 231u8, 132u8, 244u8, 236u8, + 197u8, 197u8, 152u8, 139u8, 58u8, 74u8, 213u8, 235u8, 234u8, 187u8, 39u8, + 231u8, 173u8, 34u8, 167u8, 101u8, 112u8, 18u8, 142u8, 81u8, 223u8, + ], + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::core::stringify!(AutomationCycleEvent), + ]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = &[ + ::SIGNATURE, + ]; + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 32usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector( + selector: [u8; 32usize], + ) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolEventInterface for SupraContractsBindingsEvents { + const NAME: &'static str = "SupraContractsBindingsEvents"; + const COUNT: usize = 1usize; + fn decode_raw_log( + topics: &[alloy_sol_types::Word], + data: &[u8], + ) -> alloy_sol_types::Result { + match topics.first().copied() { + Some( + ::SIGNATURE_HASH, + ) => { + ::decode_raw_log( + topics, + data, + ) + .map(Self::AutomationCycleEvent) + } + _ => { + alloy_sol_types::private::Err(alloy_sol_types::Error::InvalidLog { + name: ::NAME, + log: alloy_sol_types::private::Box::new( + alloy_sol_types::private::LogData::new_unchecked( + topics.to_vec(), + data.to_vec().into(), + ), + ), + }) + } + } + } + } + #[automatically_derived] + impl alloy_sol_types::private::IntoLogData for SupraContractsBindingsEvents { + fn to_log_data(&self) -> alloy_sol_types::private::LogData { + match self { + Self::AutomationCycleEvent(inner) => { + alloy_sol_types::private::IntoLogData::to_log_data(inner) + } + } + } + fn into_log_data(self) -> alloy_sol_types::private::LogData { + match self { + Self::AutomationCycleEvent(inner) => { + alloy_sol_types::private::IntoLogData::into_log_data(inner) + } + } + } + } + use alloy::contract as alloy_contract; + /**Creates a new wrapper around an on-chain [`SupraContractsBindings`](self) contract instance. + +See the [wrapper's documentation](`SupraContractsBindingsInstance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> SupraContractsBindingsInstance { + SupraContractsBindingsInstance::::new(address, __provider) + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + +Returns a new instance of the contract, if the deployment was successful. + +For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub fn deploy< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + __provider: P, + ) -> impl ::core::future::Future< + Output = alloy_contract::Result>, + > { + SupraContractsBindingsInstance::::deploy(__provider) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` +and constructor arguments, if any. + +This is a simple wrapper around creating a `RawCallBuilder` with the data set to +the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >(__provider: P) -> alloy_contract::RawCallBuilder { + SupraContractsBindingsInstance::::deploy_builder(__provider) + } + /**A [`SupraContractsBindings`](self) instance. + +Contains type-safe methods for interacting with an on-chain instance of the +[`SupraContractsBindings`](self) contract located at a given `address`, using a given +provider `P`. + +If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) +documentation on how to provide it), the `deploy` and `deploy_builder` methods can +be used to deploy a new instance of the contract. + +See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct SupraContractsBindingsInstance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for SupraContractsBindingsInstance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("SupraContractsBindingsInstance").field(&self.address).finish() + } + } + /// Instantiation and getters/setters. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > SupraContractsBindingsInstance { + /**Creates a new wrapper around an on-chain [`SupraContractsBindings`](self) contract instance. + +See the [wrapper's documentation](`SupraContractsBindingsInstance`) for more details.*/ + #[inline] + pub const fn new( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + +Returns a new instance of the contract, if the deployment was successful. + +For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub async fn deploy( + __provider: P, + ) -> alloy_contract::Result> { + let call_builder = Self::deploy_builder(__provider); + let contract_address = call_builder.deploy().await?; + Ok(Self::new(contract_address, call_builder.provider)) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` +and constructor arguments, if any. + +This is a simple wrapper around creating a `RawCallBuilder` with the data set to +the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder(__provider: P) -> alloy_contract::RawCallBuilder { + alloy_contract::RawCallBuilder::new_raw_deploy( + __provider, + ::core::clone::Clone::clone(&BYTECODE), + ) + } + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl SupraContractsBindingsInstance<&P, N> { + /// Clones the provider and returns a new instance with the cloned provider. + #[inline] + pub fn with_cloned_provider(self) -> SupraContractsBindingsInstance { + SupraContractsBindingsInstance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > SupraContractsBindingsInstance { + /// Creates a new call builder using this contract instance's provider and address. + /// + /// Note that the call can be any function call, not just those defined in this + /// contract. Prefer using the other methods for building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + ///Creates a new call builder for the [`blockPrologue`] function. + pub fn blockPrologue( + &self, + ) -> alloy_contract::SolCallBuilder<&P, blockPrologueCall, N> { + self.call_builder(&blockPrologueCall) + } + ///Creates a new call builder for the [`getActiveTaskIds`] function. + pub fn getActiveTaskIds( + &self, + ) -> alloy_contract::SolCallBuilder<&P, getActiveTaskIdsCall, N> { + self.call_builder(&getActiveTaskIdsCall) + } + ///Creates a new call builder for the [`getCycleStateDetails`] function. + pub fn getCycleStateDetails( + &self, + ) -> alloy_contract::SolCallBuilder<&P, getCycleStateDetailsCall, N> { + self.call_builder(&getCycleStateDetailsCall) + } + ///Creates a new call builder for the [`getTaskDetails`] function. + pub fn getTaskDetails( + &self, + _taskIndex: u64, + ) -> alloy_contract::SolCallBuilder<&P, getTaskDetailsCall, N> { + self.call_builder(&getTaskDetailsCall { _taskIndex }) + } + ///Creates a new call builder for the [`getTaskDetailsBulk`] function. + pub fn getTaskDetailsBulk( + &self, + _taskIndexes: alloy::sol_types::private::Vec, + ) -> alloy_contract::SolCallBuilder<&P, getTaskDetailsBulkCall, N> { + self.call_builder( + &getTaskDetailsBulkCall { + _taskIndexes, + }, + ) + } + ///Creates a new call builder for the [`getTaskIdList`] function. + pub fn getTaskIdList( + &self, + ) -> alloy_contract::SolCallBuilder<&P, getTaskIdListCall, N> { + self.call_builder(&getTaskIdListCall) + } + ///Creates a new call builder for the [`ifTaskExists`] function. + pub fn ifTaskExists( + &self, + _taskIndex: u64, + ) -> alloy_contract::SolCallBuilder<&P, ifTaskExistsCall, N> { + self.call_builder(&ifTaskExistsCall { _taskIndex }) + } + ///Creates a new call builder for the [`isAutomationReadyEnabled`] function. + pub fn isAutomationReadyEnabled( + &self, + ) -> alloy_contract::SolCallBuilder<&P, isAutomationReadyEnabledCall, N> { + self.call_builder(&isAutomationReadyEnabledCall) + } + ///Creates a new call builder for the [`isInitialized`] function. + pub fn isInitialized( + &self, + ) -> alloy_contract::SolCallBuilder<&P, isInitializedCall, N> { + self.call_builder(&isInitializedCall) + } + ///Creates a new call builder for the [`processTasks`] function. + pub fn processTasks( + &self, + _cycleIndex: u64, + _taskIndexes: alloy::sol_types::private::Vec< + alloy::sol_types::private::primitives::aliases::U256, + >, + ) -> alloy_contract::SolCallBuilder<&P, processTasksCall, N> { + self.call_builder( + &processTasksCall { + _cycleIndex, + _taskIndexes, + }, + ) + } + ///Creates a new call builder for the [`removeRegisteredTask`] function. + pub fn removeRegisteredTask( + &self, + _cycleIndex: u64, + _taskIndex: u64, + _reason: alloy::sol_types::private::String, + ) -> alloy_contract::SolCallBuilder<&P, removeRegisteredTaskCall, N> { + self.call_builder( + &removeRegisteredTaskCall { + _cycleIndex, + _taskIndex, + _reason, + }, + ) + } + } + /// Event filters. + impl< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + > SupraContractsBindingsInstance { + /// Creates a new event filter using this contract instance's provider and address. + /// + /// Note that the type can be any event, not just those defined in this contract. + /// Prefer using the other methods for building type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + ///Creates a new event filter for the [`AutomationCycleEvent`] event. + pub fn AutomationCycleEvent_filter( + &self, + ) -> alloy_contract::Event<&P, AutomationCycleEvent, N> { + self.event_filter::() + } + } +} diff --git a/crates/supra-extension/src/transactions/automated_transaction.rs b/crates/supra-extension/src/transactions/automated_transaction.rs new file mode 100644 index 0000000000..c55d125bdc --- /dev/null +++ b/crates/supra-extension/src/transactions/automated_transaction.rs @@ -0,0 +1,1394 @@ +//! AutomatedTransaction generated based on the registered active automation task. + +use crate::errors::SupraExtensionError; +use crate::value_or_error; +use crate::TaskMetadata; +use alloy::eips::eip2930::AccessList; +use alloy::primitives::{Address, Bytes, ChainId, B256, U256}; +use alloy_consensus::transaction::Transaction; +use alloy_eips::eip2718::Typed2718; +use alloy_sol_types::SolType; +use context::transaction::{AccessListItem, SignedAuthorization}; +use context::TransactionType; +use derive_getters::{Dissolve, Getters}; +use derive_more::Constructor; +use primitives::TxKind; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +/// Represents automation task predicate details. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct TaskPredicate { + /// Contract address holding predicate function + pub address: Address, + /// Encoded predicate function pointer and inputs + pub input: Bytes, +} + +impl TryFrom<&[u8]> for TaskPredicate { + type Error = SupraExtensionError; + + fn try_from(value: &[u8]) -> Result { + type PredicateType = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Bytes, + ); + let (address, input) = PredicateType::abi_decode_sequence(value) + .map_err(|e| SupraExtensionError::PredicateDecode { error: e })?; + Ok(Self { address, input }) + } +} + +/// Supported automation task predicates +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub enum AutomationTaskPredicate { + /// Represents always true predicate + #[default] + Bypass, + /// Predicate to be executed. + Predicate(TaskPredicate), +} + +impl TryFrom<&[u8]> for AutomationTaskPredicate { + type Error = SupraExtensionError; + + fn try_from(value: &[u8]) -> Result { + if value.is_empty() { + Ok(Self::Bypass) + } else { + Ok(Self::Predicate(TaskPredicate::try_from(value)?)) + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[repr(u8)] +/// Automated transaction type corresponding automation task type. +pub enum AutomatedTransactionType { + /// User submitted automation task based + #[default] + UST = 0, + /// Governance submitted/authorized automation task based. Will be gasless transaction + GST, +} + +impl TryFrom for AutomatedTransactionType { + type Error = SupraExtensionError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::UST), + 1 => Ok(Self::GST), + _ => Err(Self::Error::InvalidAutomationTaskTypeValue(value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +/// Automated transaction generated by node runtime based on the registered active automation task. +pub struct AutomatedTransaction { + /// Height of the block in scope of which this transaction is being executed. + pub block_height: u64, + /// Hash of the transaction which registered an automation task based on which this transaction is created. + pub registration_hash: B256, + /// Owner address of the automation task, the source of this transaction. + pub sender: Address, + /// Type of the automated transaction. + pub txn_type: AutomatedTransactionType, + /// Chain id. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub chain_id: ChainId, + /// A scalar value equal to the automation task index based on which this transaction is created. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + #[cfg_attr( + feature = "serde", + serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit") + )] + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub max_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + #[cfg_attr(feature = "serde", serde(default))] + pub to: Address, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + // Deserialize with `alloy_serde::null_as_default` to also accept a `null` value + // instead of an (empty) array. This is due to certain RPC providers (e.g., Filecoin's) + // sometimes returning `null` instead of an empty array `[]`. + // More details in . + #[cfg_attr( + feature = "serde", + serde(deserialize_with = "alloy_serde::null_as_default") + )] + pub access_list: AccessList, + /// Input: An unlimited size byte array specifying the + /// input data of the message call. + pub input: Bytes, + + /// Predicate controlling task execution. + /// If predicate execution is successful and true the main action will be executed. + pub predicate: AutomationTaskPredicate, +} + +impl Transaction for AutomatedTransaction { + #[inline] + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + #[inline] + fn nonce(&self) -> u64 { + self.nonce + } + + #[inline] + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + #[inline] + fn gas_price(&self) -> Option { + None + } + + #[inline] + fn max_fee_per_gas(&self) -> u128 { + self.max_fee_per_gas + } + + #[inline] + fn max_priority_fee_per_gas(&self) -> Option { + Some(0) + } + + #[inline] + fn max_fee_per_blob_gas(&self) -> Option { + None + } + + #[inline] + fn priority_fee_or_price(&self) -> u128 { + 0 + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + alloy_eips::eip1559::calc_effective_gas_price(self.max_fee_per_gas, 0, base_fee) + } + + #[inline] + fn is_dynamic_fee(&self) -> bool { + true + } + + #[inline] + fn kind(&self) -> TxKind { + TxKind::Call(self.to) + } + + #[inline] + fn is_create(&self) -> bool { + false + } + + #[inline] + fn value(&self) -> U256 { + self.value + } + + #[inline] + fn input(&self) -> &Bytes { + &self.input + } + + #[inline] + fn access_list(&self) -> Option<&AccessList> { + Some(&self.access_list) + } + + #[inline] + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + None + } + + #[inline] + fn authorization_list(&self) -> Option<&[SignedAuthorization]> { + None + } +} + +impl Typed2718 for AutomatedTransaction { + fn ty(&self) -> u8 { + TransactionType::Custom as u8 + } +} + +impl AutomatedTransaction { + /// Returns true if automated transaction is gas-less, otherwise false. + /// GST type transactions are considered as gas-less. + pub fn is_gasless(&self) -> bool { + matches!(self.txn_type, AutomatedTransactionType::GST) + } +} + +/// Evm automated transaction with priority to be scheduled for execution. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct AutomatedTransactionDetails { + /// Transaction details + pub txn: AutomatedTransaction, + /// Priority of the automated transaction to be scheduled. + /// The low value indicates higher priority. + pub priority: u64, +} + +impl Ord for AutomatedTransactionDetails { + fn cmp(&self, other: &Self) -> Ordering { + let left_type = &self.txn.txn_type; + let right_type = &other.txn.txn_type; + if left_type == right_type { + self.priority.cmp(&other.priority) + } else { + left_type.cmp(right_type) + } + } +} +impl PartialOrd for AutomatedTransactionDetails { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +type AccessListItemTy = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Array>, +); +type AccessListTy = alloy_sol_types::sol_data::Array; +type ExpandedPayloadTy = ( + alloy_sol_types::sol_data::Uint<256>, + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Bytes, + AccessListTy, +); + +/// Evm automation task execution payload. +#[derive( + Clone, + Debug, + Default, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Getters, + Dissolve, + Constructor, +)] +pub struct TaskPayload { + to: Address, + value: U256, + input: Bytes, + access_list: AccessList, +} + +impl TaskPayload { + /// Generates random [`TaskPayload`] for testing propose only. + pub fn random() -> Self { + TaskPayload { + to: Address::random(), + value: U256::random(), + input: Bytes::from(b"test"), + access_list: AccessList::default(), + } + } +} + +impl TryFrom<&[u8]> for TaskPayload { + type Error = SupraExtensionError; + + fn try_from(value: &[u8]) -> Result { + let (value, to, input, access_list) = ExpandedPayloadTy::abi_decode_sequence(value) + .map_err(|e| SupraExtensionError::PayloadDecode { error: e })?; + let access_items = access_list + .into_iter() + .map(|(address, storage_keys)| AccessListItem { + address, + storage_keys, + }) + .collect(); + Ok(Self { + to, + value, + access_list: AccessList(access_items), + input, + }) + } +} + +/// Automation task state in native layer +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +enum AutomationTaskState { + Pending = 0, + Active = 1, + Cancelled = 2, +} + +impl TryFrom for AutomationTaskState { + type Error = SupraExtensionError; + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Pending), + 1 => Ok(Self::Active), + 2 => Ok(Self::Cancelled), + _ => Err(SupraExtensionError::InvalidAutomationTaskStateValue(value)), + } + } +} + +/// [`AutomatedTransactionBuilder`] result +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuildResult { + /// Success result wrapping [`AutomatedTransactionDetails`] + Success(AutomatedTransactionDetails), + /// Build failure due to gas-price limit surpass. + GasPriceLimitExceeded { + /// Task index for which error is observed. + task_index: u64, + /// Gas price specified for the transaction. + value: u128, + /// Gas price threshold specified for the automation task during registration. + threshold: u128, + }, +} + +/// Builder for [`AutomatedTransactionDetails`] +/// All properties are mandatory to be set for successful automated transaction build. +/// The following values fall back to agreed defaults if not specified: +/// - priority - defaults to task-index +/// - access_list - default to empty access-list +/// - value - defaults to 0 +#[derive(Clone, Debug, Getters)] +pub struct AutomatedTransactionBuilder { + block_height: Option, + chain_id: Option, + gas_limit: Option, + gas_price: Option, + gas_price_cap: Option, + registration_hash: Option, + task_index: Option, + expiry_timestamp: Option, + owner: Option
, + typ: Option, + priority: Option, + + to: Option
, + value: Option, + access_list: Option, + input: Option, + predicate: Option, +} + +#[allow(missing_docs)] +impl AutomatedTransactionBuilder { + pub fn new() -> Self { + Self { + block_height: None, + chain_id: None, + gas_limit: None, + gas_price: None, + gas_price_cap: None, + registration_hash: None, + task_index: None, + expiry_timestamp: None, + owner: None, + typ: None, + priority: None, + to: None, + value: Some(U256::from(0)), + access_list: Some(AccessList::default()), + input: None, + predicate: None, + } + } + + pub fn with_block_height(mut self, block_height: u64) -> Self { + self.block_height = Some(block_height); + self + } + + pub fn with_chain_id(mut self, chain_id: ChainId) -> Self { + self.chain_id = Some(chain_id); + self + } + + pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + pub fn with_gas_price(mut self, gas_price: u128) -> Self { + self.gas_price = Some(gas_price); + self + } + + pub fn with_gas_price_cap(mut self, gas_price_cap: u128) -> Self { + self.gas_price_cap = Some(gas_price_cap); + self + } + + pub fn with_registration_hash(mut self, registration_hash: B256) -> Self { + self.registration_hash = Some(registration_hash); + self + } + + pub fn with_task_index(mut self, task_index: u64) -> Self { + self.task_index = Some(task_index); + self + } + + pub fn with_expiry_timestamp(mut self, expiry_timestamp: u64) -> Self { + self.expiry_timestamp = Some(expiry_timestamp); + self + } + + pub fn with_owner(mut self, owner: Address) -> Self { + self.owner = Some(owner); + self + } + pub fn with_typ(mut self, typ: AutomatedTransactionType) -> Self { + self.typ = Some(typ); + self + } + + pub fn with_priority(mut self, priority: u64) -> Self { + self.priority = Some(priority); + self + } + + pub fn with_to(mut self, to: Address) -> Self { + self.to = Some(to); + self + } + + pub fn with_value(mut self, value: U256) -> Self { + self.value = Some(value); + self + } + + pub fn with_access_list(mut self, access_list: AccessList) -> Self { + self.access_list = Some(access_list); + self + } + + pub fn with_input(mut self, input: Bytes) -> Self { + self.input = Some(input); + self + } + + pub fn with_predicate(mut self, predicate: AutomationTaskPredicate) -> Self { + self.predicate = Some(predicate); + self + } + + pub fn build(self) -> Result { + let Self { + block_height, + chain_id, + gas_limit, + gas_price, + gas_price_cap, + registration_hash, + task_index, + expiry_timestamp: _, + owner, + typ, + priority, + to, + value, + access_list, + input, + predicate, + } = self; + let typ = value_or_error!(AutomatedTransactionBuilder, "type", typ); + let block_height = + value_or_error!(AutomatedTransactionBuilder, "block_height", block_height); + let chain_id = value_or_error!(AutomatedTransactionBuilder, "chain_id", chain_id); + let gas_limit = value_or_error!(AutomatedTransactionBuilder, "gas_limit", gas_limit); + let gas_price_cap = + value_or_error!(AutomatedTransactionBuilder, "gas_price_cap", gas_price_cap); + // GST based automated transaction are not charged, so the gas price is not mandatory and default to 0. + let gas_price = match typ { + AutomatedTransactionType::UST => { + value_or_error!(AutomatedTransactionBuilder, "gas_price", gas_price) + } + AutomatedTransactionType::GST => 0, + }; + let registration_hash = value_or_error!( + AutomatedTransactionBuilder, + "registration_hash", + registration_hash + ); + let task_index = value_or_error!(AutomatedTransactionBuilder, "task_index", task_index); + let owner = value_or_error!(AutomatedTransactionBuilder, "owner", owner); + let priority = priority.unwrap_or(task_index); + let to = value_or_error!(AutomatedTransactionBuilder, "to", to); + let value = value_or_error!(AutomatedTransactionBuilder, "value", value); + let access_list = value_or_error!(AutomatedTransactionBuilder, "access_list", access_list); + let input = value_or_error!(AutomatedTransactionBuilder, "input", input); + if typ == AutomatedTransactionType::UST && gas_price_cap < gas_price { + return Ok(BuildResult::GasPriceLimitExceeded { + task_index, + value: gas_price, + threshold: gas_price_cap, + }); + } + let txn = AutomatedTransaction { + block_height, + registration_hash, + sender: owner, + txn_type: typ, + chain_id, + nonce: task_index, + gas_limit, + max_fee_per_gas: gas_price, + to, + value, + access_list, + input, + predicate: predicate.unwrap_or_default(), + }; + Ok(BuildResult::Success(AutomatedTransactionDetails { + txn, + priority, + })) + } +} + +/// Constructs [`AutomatedTransactionBuilder`] from automation task details loaded from chain state. +/// Fails if: +/// - inner payload cannot be deserialized based on the [`ExpandedPayloadTy`] schema +/// - Loaded task is not in active state (Active | Cancelled) +impl TryFrom for AutomatedTransactionBuilder { + type Error = SupraExtensionError; + + fn try_from(value: TaskMetadata) -> Result { + let TaskMetadata { + maxGasAmount, + gasPriceCap, + automationFeeCapForCycle: _, + depositFee: _, + txHash, + taskIndex, + registrationTime: _, + expiryTime, + priority, + taskType, + owner, + taskState, + payloadTx, + predicate, + auxData: _, + } = value; + + if AutomationTaskState::try_from(taskState)? == AutomationTaskState::Pending { + return Err(SupraExtensionError::InvalidAutomationTaskStateForBuilder); + } + let typ = AutomatedTransactionType::try_from(taskType)?; + + let (to, value, input, access_list) = TaskPayload::try_from(payloadTx.as_ref())?.dissolve(); + let predicate = AutomationTaskPredicate::try_from(predicate.as_ref())?; + let builder = Self::new() + .with_gas_price_cap(gasPriceCap) + .with_gas_limit(maxGasAmount as u64) + .with_gas_price_cap(gasPriceCap) + .with_registration_hash(txHash) + .with_task_index(taskIndex) + .with_expiry_timestamp(expiryTime) + .with_owner(owner) + .with_to(to) + .with_value(value) + .with_input(input) + .with_access_list(access_list) + .with_typ(typ) + .with_priority(priority) + .with_predicate(predicate); + Ok(builder) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::transactions::automated_transaction::{ExpandedPayloadTy, TaskPredicate}; + use crate::{errors::SupraExtensionError, TaskMetadata}; + use alloy::hex; + use alloy::primitives::{address, b256, Address, Bytes, B256, U256}; + use alloy_consensus::transaction::Transaction; + use alloy_sol_types::SolType; + + type PredicateType = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Bytes, + ); + + const OWNER: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + const TO: Address = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + const CHAIN_ID: u64 = 6; + const BLOCK_HEIGHT: u64 = 200; + const TASK_INDEX: u64 = 7; + const GAS_LIMIT: u64 = 500_000; + const GAS_PRICE: u128 = 1_000; + const GAS_PRICE_CAP: u128 = 2_000; + const REG_HASH: B256 = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + fn encode_payload(value: U256, to: Address, input: &[u8]) -> Bytes { + Bytes::from(ExpandedPayloadTy::abi_encode_sequence(&( + value, + to, + Bytes::from(input.to_vec()), + vec![] as Vec<(Address, Vec)>, + ))) + } + + fn encode_predicate(addr: Address, input: &[u8]) -> Bytes { + Bytes::from(PredicateType::abi_encode_sequence(&( + addr, + Bytes::from(input.to_vec()), + ))) + } + + fn base_task_metadata(task_state: u8, task_type: u8) -> TaskMetadata { + TaskMetadata { + maxGasAmount: GAS_LIMIT as u128, + gasPriceCap: GAS_PRICE_CAP, + automationFeeCapForCycle: 0, + depositFee: 0, + txHash: REG_HASH, + taskIndex: TASK_INDEX, + registrationTime: 0, + expiryTime: 0, + priority: TASK_INDEX, + owner: OWNER, + taskType: task_type, + taskState: task_state, + payloadTx: encode_payload(U256::ZERO, TO, b"call"), + predicate: Bytes::default(), + auxData: vec![], + } + } + + fn base_ust_builder() -> AutomatedTransactionBuilder { + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"calldata")) + } + + fn base_gst_builder() -> AutomatedTransactionBuilder { + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::GST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"calldata")) + } + + fn unwrap_success(r: BuildResult) -> AutomatedTransactionDetails { + match r { + BuildResult::Success(d) => d, + other => panic!("expected BuildResult::Success, got {other:?}"), + } + } + + // ── TaskPredicate::try_from ─────────────────────────────────────────────── + + #[test] + fn predicate_from_empty_slice_returns_error() { + let p = TaskPredicate::try_from([].as_slice()); + assert!(matches!( + p, + Err(SupraExtensionError::PredicateDecode { .. }) + )); + } + + #[test] + fn automation_task_predicate_from_empty_slice_returns_bypass() { + let p = AutomationTaskPredicate::try_from([].as_slice()).unwrap(); + assert!(matches!(p, AutomationTaskPredicate::Bypass)); + } + + #[test] + fn predicate_from_valid_bytes_decodes_correctly() { + let pred_addr = address!("cccccccccccccccccccccccccccccccccccccccc"); + let encoded = encode_predicate(pred_addr, b"check_fn"); + let p = TaskPredicate::try_from(encoded.as_ref()).unwrap(); + assert_eq!(p.address, pred_addr); + assert_eq!(p.input, Bytes::from(b"check_fn")); + } + + #[test] + fn predicate_from_invalid_bytes_returns_payload_decode_error() { + let err = TaskPredicate::try_from([0xFF, 0x01, 0x02].as_slice()).unwrap_err(); + assert!(matches!(err, SupraExtensionError::PredicateDecode { .. })); + } + + // ── AutomatedTransactionType::try_from ─────────────────────────────────── + + #[test] + fn txn_type_checks() { + // 0 is UST + assert_eq!( + AutomatedTransactionType::try_from(0u8).unwrap(), + AutomatedTransactionType::UST + ); + // 1 is GST + assert_eq!( + AutomatedTransactionType::try_from(1u8).unwrap(), + AutomatedTransactionType::GST + ); + + // Any other value is error + for v in [2u8, 50, 255] { + assert!( + matches!( + AutomatedTransactionType::try_from(v), + Err(SupraExtensionError::InvalidAutomationTaskTypeValue(_)) + ), + "expected error for {v}" + ); + } + } + + // ── AutomatedTransaction::is_gasless ────────────────────────────────────── + + #[test] + fn ust_is_not_gasless() { + let txn = AutomatedTransaction { + txn_type: AutomatedTransactionType::UST, + ..Default::default() + }; + assert!(!txn.is_gasless()); + } + + #[test] + fn gst_is_gasless() { + let txn = AutomatedTransaction { + txn_type: AutomatedTransactionType::GST, + ..Default::default() + }; + assert!(txn.is_gasless()); + } + + // ── Transaction trait impl ──────────────────────────────────────────────── + + #[test] + fn transaction_trait_field_accessors() { + let input_data = Bytes::from(b"calldata"); + let val = U256::from(999u64); + let txn = AutomatedTransaction { + block_height: BLOCK_HEIGHT, + registration_hash: REG_HASH, + sender: OWNER, + txn_type: AutomatedTransactionType::UST, + chain_id: CHAIN_ID, + nonce: TASK_INDEX, + gas_limit: GAS_LIMIT, + max_fee_per_gas: GAS_PRICE, + to: TO, + value: val, + access_list: AccessList::default(), + input: input_data.clone(), + predicate: AutomationTaskPredicate::Bypass, + }; + + assert_eq!(txn.chain_id(), Some(CHAIN_ID)); + assert_eq!(txn.nonce(), TASK_INDEX); + assert_eq!(txn.gas_limit(), GAS_LIMIT); + assert_eq!(txn.gas_price(), None); + assert_eq!(txn.max_fee_per_gas(), GAS_PRICE); + assert_eq!(txn.max_priority_fee_per_gas(), Some(0)); + assert_eq!(txn.max_fee_per_blob_gas(), None); + assert_eq!(txn.priority_fee_or_price(), 0); + assert!(txn.is_dynamic_fee()); + assert_eq!(txn.kind(), TxKind::Call(TO)); + assert!(!txn.is_create()); + assert_eq!(txn.value(), val); + assert_eq!(txn.input(), &input_data); + assert_eq!(txn.access_list(), Some(&AccessList::default())); + assert_eq!(txn.blob_versioned_hashes(), None); + assert_eq!(txn.authorization_list(), None); + } + + #[test] + fn effective_gas_price_no_base_fee_equals_max_fee() { + let txn = AutomatedTransaction { + max_fee_per_gas: 5_000, + ..Default::default() + }; + assert_eq!(txn.effective_gas_price(None), 5_000); + } + + #[test] + fn effective_gas_price_base_fee_below_max_fee_uses_base_fee() { + let txn = AutomatedTransaction { + max_fee_per_gas: 5_000, + ..Default::default() + }; + // min(5000, 100 + 0) = 100 + assert_eq!(txn.effective_gas_price(Some(100)), 100); + } + + #[test] + fn effective_gas_price_base_fee_above_max_fee_is_capped() { + let txn = AutomatedTransaction { + max_fee_per_gas: 1_000, + ..Default::default() + }; + // min(1000, 9999 + 0) = 1000 + assert_eq!(txn.effective_gas_price(Some(9_999)), 1_000); + } + + // ── AutomatedTransactionDetails ordering ────────────────────────────────── + + fn make_details( + txn_type: AutomatedTransactionType, + priority: u64, + ) -> AutomatedTransactionDetails { + AutomatedTransactionDetails { + txn: AutomatedTransaction { + txn_type, + ..Default::default() + }, + priority, + } + } + + #[test] + fn same_type_ordered_by_priority_ascending() { + let low = make_details(AutomatedTransactionType::UST, 1); + let high = make_details(AutomatedTransactionType::UST, 10); + assert!(low < high); + assert!(high > low); + assert_eq!(low.cmp(&low), std::cmp::Ordering::Equal); + } + + #[test] + fn ust_always_less_than_gst_regardless_of_priority() { + let ust = make_details(AutomatedTransactionType::UST, 100); + let gst = make_details(AutomatedTransactionType::GST, 1); + assert!(ust < gst); + } + + #[test] + fn gst_always_greater_than_ust() { + let ust = make_details(AutomatedTransactionType::UST, 0); + let gst = make_details(AutomatedTransactionType::GST, u64::MAX); + assert!(gst > ust); + } + + // ── TaskPayload ─────────────────────────────────────────────────────────── + + #[test] + fn task_payload_from_valid_bytes_decodes_all_fields() { + let expected_value = U256::from(42u64); + let encoded = encode_payload(expected_value, TO, b"hello"); + let payload = TaskPayload::try_from(encoded.as_ref()).unwrap(); + assert_eq!(*payload.to(), TO); + assert_eq!(*payload.value(), expected_value); + assert_eq!(*payload.input(), Bytes::from(b"hello")); + assert!(payload.access_list().0.is_empty()); + } + + #[test] + fn task_payload_from_invalid_bytes_returns_error() { + let err = TaskPayload::try_from([0xDE, 0xAD, 0xBE].as_slice()).unwrap_err(); + assert!(matches!(err, SupraExtensionError::PayloadDecode { .. })); + } + + #[test] + fn task_payload_random_produces_non_zero_address() { + let payload = TaskPayload::random(); + assert_ne!(*payload.to(), Address::ZERO); + assert_ne!(*payload.value(), U256::ZERO); + } + + // ── AutomatedTransactionBuilder: successful UST build ───────────────────── + + #[test] + fn build_ust_success_sets_all_fields() { + let details = unwrap_success(base_ust_builder().build().unwrap()); + let txn = &details.txn; + + assert_eq!(txn.block_height, BLOCK_HEIGHT); + assert_eq!(txn.registration_hash, REG_HASH); + assert_eq!(txn.sender, OWNER); + assert_eq!(txn.txn_type, AutomatedTransactionType::UST); + assert_eq!(txn.chain_id, CHAIN_ID); + assert_eq!(txn.nonce, TASK_INDEX); + assert_eq!(txn.gas_limit, GAS_LIMIT); + assert_eq!(txn.max_fee_per_gas, GAS_PRICE); + assert_eq!(txn.to, TO); + assert_eq!(details.priority, TASK_INDEX); + } + + #[test] + fn build_gst_defaults_gas_price_to_zero() { + let details = unwrap_success(base_gst_builder().build().unwrap()); + assert_eq!(details.txn.max_fee_per_gas, 0); + assert!(details.txn.is_gasless()); + } + + #[test] + fn build_priority_defaults_to_task_index() { + let details = unwrap_success(base_ust_builder().build().unwrap()); + assert_eq!(details.priority, TASK_INDEX); + } + + #[test] + fn build_explicit_priority_overrides_default() { + let details = unwrap_success(base_ust_builder().with_priority(42).build().unwrap()); + assert_eq!(details.priority, 42); + } + + #[test] + fn build_without_predicate_sets_bypass() { + let details = unwrap_success(base_ust_builder().build().unwrap()); + assert_eq!(details.txn.predicate, AutomationTaskPredicate::Bypass); + } + + #[test] + fn build_with_predicate_sets_predicate_variant() { + let pred = TaskPredicate { + address: address!("cccccccccccccccccccccccccccccccccccccccc"), + input: Bytes::from(b"check"), + }; + let details = unwrap_success( + base_ust_builder() + .with_predicate(AutomationTaskPredicate::Predicate(pred.clone())) + .build() + .unwrap(), + ); + assert_eq!( + details.txn.predicate, + AutomationTaskPredicate::Predicate(pred) + ); + } + + // ── AutomatedTransactionBuilder: gas price cap check ───────────────────── + + #[test] + fn build_ust_gas_price_above_cap_returns_exceeded() { + let result = base_ust_builder() + .with_gas_price(3_000) + .with_gas_price_cap(1_000) + .build() + .unwrap(); + match result { + BuildResult::GasPriceLimitExceeded { + task_index, + value, + threshold, + } => { + assert_eq!(task_index, TASK_INDEX); + assert_eq!(value, 3_000); + assert_eq!(threshold, 1_000); + } + _ => panic!("expected GasPriceLimitExceeded"), + } + } + + #[test] + fn build_ust_gas_price_equal_to_cap_returns_success() { + let result = base_ust_builder() + .with_gas_price(1_000) + .with_gas_price_cap(1_000) + .build() + .unwrap(); + assert!(matches!(result, BuildResult::Success(_))); + } + + #[test] + fn build_ust_gas_price_below_cap_returns_success() { + let result = base_ust_builder() + .with_gas_price(500) + .with_gas_price_cap(1_000) + .build() + .unwrap(); + assert!(matches!(result, BuildResult::Success(_))); + } + + #[test] + fn build_gst_ignores_gas_price_cap_check() { + // GST gas_price is forced to 0 so even cap = 0 never triggers exceeded + let result = base_gst_builder().with_gas_price_cap(0).build().unwrap(); + assert!(matches!(result, BuildResult::Success(_))); + } + + // ── AutomatedTransactionBuilder: missing mandatory fields ───────────────── + + macro_rules! missing_field_test { + ($name:ident, $builder:expr, $field:literal) => { + #[test] + fn $name() { + let err = $builder.build().unwrap_err(); + assert!( + matches!(&err, SupraExtensionError::MissingBuilderValue(_, f) if f == $field), + "expected MissingBuilderValue for field {}, got {err:?}", $field + ); + } + }; + } + + missing_field_test!( + build_missing_type_returns_error, + AutomatedTransactionBuilder::new() + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "type" + ); + + missing_field_test!( + build_missing_block_height_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "block_height" + ); + + missing_field_test!( + build_missing_chain_id_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "chain_id" + ); + + missing_field_test!( + build_missing_gas_limit_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "gas_limit" + ); + + missing_field_test!( + build_missing_gas_price_cap_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "gas_price_cap" + ); + + missing_field_test!( + build_ust_missing_gas_price_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "gas_price" + ); + + missing_field_test!( + build_missing_registration_hash_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "registration_hash" + ); + + missing_field_test!( + build_missing_task_index_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_owner(OWNER) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "task_index" + ); + + missing_field_test!( + build_missing_owner_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_to(TO) + .with_input(Bytes::from(b"d")), + "owner" + ); + + missing_field_test!( + build_missing_to_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_input(Bytes::from(b"d")), + "to" + ); + + missing_field_test!( + build_missing_input_returns_error, + AutomatedTransactionBuilder::new() + .with_typ(AutomatedTransactionType::UST) + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_limit(GAS_LIMIT) + .with_gas_price(GAS_PRICE) + .with_gas_price_cap(GAS_PRICE_CAP) + .with_registration_hash(REG_HASH) + .with_task_index(TASK_INDEX) + .with_owner(OWNER) + .with_to(TO), + "input" + ); + + // ── TryFrom ───────────────────────────────────────────────── + + #[test] + fn task_metadata_pending_state_returns_error() { + let metadata = base_task_metadata(0, 0); // Pending, UST + let err = AutomatedTransactionBuilder::try_from(metadata).unwrap_err(); + assert!(matches!( + err, + SupraExtensionError::InvalidAutomationTaskStateForBuilder + )); + } + + #[test] + fn task_metadata_invalid_state_returns_error() { + let metadata = base_task_metadata(3, 0); // invalid state + let err = AutomatedTransactionBuilder::try_from(metadata).unwrap_err(); + assert!(matches!( + err, + SupraExtensionError::InvalidAutomationTaskStateValue(3) + )); + } + + #[test] + fn task_metadata_invalid_type_returns_error() { + let metadata = base_task_metadata(1, 5); // Active, invalid type + let err = AutomatedTransactionBuilder::try_from(metadata).unwrap_err(); + assert!(matches!( + err, + SupraExtensionError::InvalidAutomationTaskTypeValue(5) + )); + } + + #[test] + fn task_metadata_active_ust_produces_correct_builder_fields() { + let metadata = base_task_metadata(1, 0); // Active, UST + let builder = AutomatedTransactionBuilder::try_from(metadata).unwrap(); + assert_eq!(*builder.task_index(), Some(TASK_INDEX)); + assert_eq!(*builder.gas_limit(), Some(GAS_LIMIT)); + assert_eq!(*builder.gas_price_cap(), Some(GAS_PRICE_CAP)); + assert_eq!(*builder.owner(), Some(OWNER)); + assert!(matches!(builder.typ(), Some(AutomatedTransactionType::UST))); + assert_eq!(*builder.to(), Some(TO)); + } + + #[test] + fn task_metadata_cancelled_gst_sets_correct_type() { + let metadata = base_task_metadata(2, 1); // Cancelled, GST + let builder = AutomatedTransactionBuilder::try_from(metadata).unwrap(); + assert!(matches!(builder.typ(), Some(AutomatedTransactionType::GST))); + } + + #[test] + fn task_metadata_with_predicate_propagates_to_builder() { + let pred_addr = address!("dddddddddddddddddddddddddddddddddddddddd"); + let mut metadata = base_task_metadata(1, 0); + metadata.predicate = encode_predicate(pred_addr, b"pred_input"); + let builder = AutomatedTransactionBuilder::try_from(metadata).unwrap(); + let predicate = builder.predicate().as_ref().unwrap(); + match predicate { + AutomationTaskPredicate::Bypass => { + panic!("Expected valid predicate got Bypass"); + } + AutomationTaskPredicate::Predicate(p) => { + assert_eq!(p.address, pred_addr); + assert_eq!(p.input, Bytes::from(b"pred_input")); + } + } + } + + #[test] + fn task_metadata_empty_predicate_produces_default_predicate_in_builder() { + // Empty predicate bytes → AutomationTaskPredicate::Bypass + // which is stored as Some(TaskPredicate::default()) in the builder + let metadata = base_task_metadata(1, 0); + let builder = AutomatedTransactionBuilder::try_from(metadata).unwrap(); + assert!(matches!( + *builder.predicate(), + Some(AutomationTaskPredicate::Bypass) + )); + } + + #[test] + fn task_metadata_invalid_payload_returns_error() { + let mut metadata = base_task_metadata(1, 0); + metadata.payloadTx = Bytes::from(b"not_valid_abi"); + let err = AutomatedTransactionBuilder::try_from(metadata).unwrap_err(); + assert!(matches!(err, SupraExtensionError::PayloadDecode { .. })); + } + + #[test] + fn task_metadata_invalid_predicate_returns_error() { + let mut metadata = base_task_metadata(1, 0); + metadata.predicate = Bytes::from(b"not_valid_abi"); + let err = AutomatedTransactionBuilder::try_from(metadata).unwrap_err(); + assert!(matches!(err, SupraExtensionError::PredicateDecode { .. })); + } + + #[test] + fn task_metadata_active_ust_full_build_succeeds() { + let metadata = base_task_metadata(1, 0); + let details = unwrap_success( + AutomatedTransactionBuilder::try_from(metadata) + .unwrap() + .with_block_height(BLOCK_HEIGHT) + .with_chain_id(CHAIN_ID) + .with_gas_price(GAS_PRICE) + .build() + .unwrap(), + ); + assert_eq!(details.txn.txn_type, AutomatedTransactionType::UST); + assert_eq!(details.txn.to, TO); + assert_eq!(details.txn.nonce, TASK_INDEX); + } + + #[test] + fn check_payload_decode() { + let encoded = hex!("00000000000000000000000000000000000000000000000000000000000000110000000000000000000000006b182f1488e8efeb2eb298155ed5bd7ff8a14042000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000242e1a7d4d0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000022220000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"); + let (value, to, input, access_list) = + ExpandedPayloadTy::abi_decode_sequence(&encoded).unwrap(); + println!("to: {:?}", to); + println!("value: {:?}", value); + println!("access_list: {:?}", access_list); + println!("input: {:?}", input); + } + + #[test] + fn check_predicate_decode() { + let encoded = hex!("000000000000000000000000d3e2a56659d5113fa44c3d09dc21ea8ff48452570000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000426a7076400000000000000000000000000000000000000000000000000000000"); + let raw_predicate = TaskPredicate::try_from(encoded.as_slice()).unwrap(); + let tagged_predicate = AutomationTaskPredicate::try_from(encoded.as_slice()).unwrap(); + assert!( + matches!(tagged_predicate, AutomationTaskPredicate::Predicate(p) if p == raw_predicate) + ); + println!("to: {:?}", raw_predicate.address); + println!("input: {:?}", raw_predicate.input); + } +} diff --git a/crates/supra-extension/src/transactions/automation_record.rs b/crates/supra-extension/src/transactions/automation_record.rs new file mode 100644 index 0000000000..d9a6edfba0 --- /dev/null +++ b/crates/supra-extension/src/transactions/automation_record.rs @@ -0,0 +1,899 @@ +//! Automation registry transaction record definition to assist automation bookkeeping. + +use crate::errors::SupraExtensionError; +use crate::{processTasksCall, removeRegisteredTaskCall, value_or_error}; +use alloy::eips::eip2930::AccessList; +use alloy::primitives::{Address, Bytes, ChainId, TxKind, B256, U256}; +use alloy_consensus::constants::SELECTOR_LEN; +use alloy_consensus::transaction::Transaction; +use alloy_eips::eip2718::Typed2718; +use alloy_sol_types::SolCall; +use context::transaction::SignedAuthorization; +use context::TransactionType; +use enum_kinds::EnumKind; +use primitives::supra_constants::VM_SIGNER; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +/// Transaction representing automation transaction record which will trigger automation task processing +/// during cycle transitions assisting automation bookkeeping flow. +pub struct AutomationRegistryRecord { + /// Address of the transaction sender. By default, it will be [`VM_SIGNER`] reserved addressed by supra. + pub sender: Address, + /// Height of the block in scope of which this transaction is being executed. + pub block_height: u64, + /// Chain id. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub chain_id: ChainId, + /// Index of the automation record being executed in scope of the block. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. Automation record execution will be gas-less, but it still will be guarded + /// by gas limit. + #[cfg_attr( + feature = "serde", + serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit") + )] + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient. + /// It will correspond to the address of the automation-registry/automation-controller SC deployed + /// by governance. + #[cfg_attr(feature = "serde", serde(default))] + pub to: Address, + /// Expected input data of the transaction + /// - Selector of automation registry record executor + /// - Index of the cycle for which automation registry record is scheduled for execution. + /// - List of the task indexes to be processed + pub input: Bytes, +} + +impl AutomationRegistryRecord { + /// Attempts to convert input bytes of [`AutomationRegistryRecord`] to [`AutomationRegistryAction`] + pub fn try_convert_to_action(&self) -> Result { + if self.input.len() < SELECTOR_LEN { + return Err(SupraExtensionError::InvalidAutomationRecord( + "Invalid input, not enough bytes for selector".to_string(), + )); + }; + let selector = &self.input[..SELECTOR_LEN]; + if removeRegisteredTaskCall::SELECTOR.as_slice().eq(selector) { + removeRegisteredTaskCall::abi_decode(&self.input) + .map_err(|e| SupraExtensionError::RecordRemoveDecode { error: e }) + .map(AutomationRecordAction::Remove) + } else if processTasksCall::SELECTOR.as_slice().eq(selector) { + processTasksCall::abi_decode(&self.input) + .map_err(|e| SupraExtensionError::RecordProcessDecode { error: e }) + .map(AutomationRecordAction::Process) + } else { + Err(SupraExtensionError::InvalidAutomationRecord(format!( + "Unrecognized selector: {selector:?}" + ))) + } + } + + /// Attempts to deduce [`AutomationRecordActionTag`] form input bytes of [`AutomationRegistryRecord`] + pub fn try_get_action_tag(&self) -> Result { + if self.input.len() < SELECTOR_LEN { + return Err(SupraExtensionError::InvalidAutomationRecord( + "Invalid input, not enough bytes for selector".to_string(), + )); + }; + let selector = &self.input[..SELECTOR_LEN]; + if removeRegisteredTaskCall::SELECTOR.as_slice().eq(selector) { + Ok(AutomationRecordActionTag::Remove) + } else if processTasksCall::SELECTOR.as_slice().eq(selector) { + Ok(AutomationRecordActionTag::Process) + } else { + Err(SupraExtensionError::InvalidAutomationRecord(format!( + "Unrecognized selector: {selector:?}" + ))) + } + } +} + +impl Transaction for AutomationRegistryRecord { + #[inline] + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + #[inline] + fn nonce(&self) -> u64 { + self.nonce + } + + #[inline] + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + #[inline] + fn gas_price(&self) -> Option { + None + } + + #[inline] + fn max_fee_per_gas(&self) -> u128 { + 0 + } + + #[inline] + fn max_priority_fee_per_gas(&self) -> Option { + Some(0) + } + + #[inline] + fn max_fee_per_blob_gas(&self) -> Option { + None + } + + #[inline] + fn priority_fee_or_price(&self) -> u128 { + 0 + } + + fn effective_gas_price(&self, _base_fee: Option) -> u128 { + 0 + } + + #[inline] + fn is_dynamic_fee(&self) -> bool { + false + } + + #[inline] + fn kind(&self) -> TxKind { + TxKind::Call(self.to) + } + + #[inline] + fn is_create(&self) -> bool { + false + } + + #[inline] + fn value(&self) -> U256 { + U256::from(0) + } + + #[inline] + fn input(&self) -> &Bytes { + &self.input + } + + #[inline] + fn access_list(&self) -> Option<&AccessList> { + None + } + + #[inline] + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + None + } + + #[inline] + fn authorization_list(&self) -> Option<&[SignedAuthorization]> { + None + } +} + +impl Typed2718 for AutomationRegistryRecord { + fn ty(&self) -> u8 { + TransactionType::Custom as u8 + } +} + +/// Action to be preformed automation registry record +/// +/// Each variant wraps the `sol!`-generated call struct for one public AutomationRegistry facet +/// function verbatim (`processTasksCall`/`removeRegisteredTaskCall`). +/// +/// Policy: once a facet function is released, its public signature is append-only for the +/// lifetime of that function - never change an existing parameter list, type, or ordering. +/// Implement any future feature or behavior change as a new facet function with its own selector +/// (and, on this Rust side, a new variant of this enum), not as an in-place edit to an existing +/// one. This is what lets the facets be redeployed independently of every downstream integration +/// already built against a released signature, without requiring any of them to coordinate a +/// simultaneous upgrade to stay compatible. +/// +/// This matters even for downstreams with no visibility into this crate at all: a consumer that +/// persists this enum via a positional, untagged encoding (e.g. BCS) can safely observe a new +/// variant being appended, but has no way to detect a field added to, removed from, or reordered +/// within an *existing* variant's wrapped struct - such a change would decode to a +/// valid-but-wrong value rather than failing loudly. Treating every released signature as +/// immutable is what makes that kind of downstream safe by construction, without it needing to +/// know or check anything about how the AutomationRegistry ABI evolves. +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumKind)] +#[enum_kind(AutomationRecordActionTag)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum AutomationRecordAction { + /// Process the tasks during cycle transition. + Process(processTasksCall), + /// Remove the task with specified index due to the reason provided by the runtime. + Remove(removeRegisteredTaskCall), +} + +impl AutomationRecordAction { + /// Crate process action with provided cycle index and list of task indexes to be processed. + pub fn process(cycle_index: u64, task_indexes: Vec) -> Self { + Self::Process(processTasksCall { + _cycleIndex: cycle_index, + _taskIndexes: task_indexes.into_iter().map(U256::from).collect(), + }) + } + + /// Crate remove action with provided cycle index and list of task indexes to be processed. + pub fn remove(cycle_index: u64, task_index: u64, reason: String) -> Self { + Self::Remove(removeRegisteredTaskCall { + _cycleIndex: cycle_index, + _taskIndex: task_index, + _reason: reason, + }) + } + + /// Converts to vector of task indexes to be handled by action. + pub fn into_task_indexes(self) -> Vec { + match self { + AutomationRecordAction::Process(task) => task + ._taskIndexes + .iter() + .map(|t| t.saturating_to::()) + .collect(), + AutomationRecordAction::Remove(task) => vec![task._taskIndex], + } + } + + /// Number of tasks to be handled by action. + pub fn task_count(&self) -> usize { + match self { + AutomationRecordAction::Process(task) => task._taskIndexes.len(), + AutomationRecordAction::Remove { .. } => 1, + } + } + + /// Flattens action to be single task if multiple tasks are configured to be processed. + pub fn flatten(self) -> Vec { + match self { + AutomationRecordAction::Process(task) => { + let cycle_index = task._cycleIndex; + task._taskIndexes + .into_iter() + .map(|t| { + AutomationRecordAction::Process(processTasksCall { + _cycleIndex: cycle_index, + _taskIndexes: vec![t], + }) + }) + .collect() + } + AutomationRecordAction::Remove { .. } => vec![self], + } + } + + /// Returns minimum and maximum task indexes configured to be processed. + pub fn task_range(&self) -> (u64, u64) { + match self { + AutomationRecordAction::Process(task) => ( + task._taskIndexes + .iter() + .min() + .map(|t| t.saturating_to::()) + .unwrap_or(u64::MAX), + task._taskIndexes + .iter() + .max() + .map(|t| t.saturating_to::()) + .unwrap_or(u64::MAX), + ), + AutomationRecordAction::Remove(task) => (task._taskIndex, task._taskIndex), + } + } + + /// Converts into abi encoded bytes. + pub fn into_bytes(self) -> Bytes { + match self { + AutomationRecordAction::Process(task) => task.abi_encode().into(), + AutomationRecordAction::Remove(task) => task.abi_encode().into(), + } + } +} + +/// Builder for [`AutomationRegistryRecord`] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct AutomationRecordBuilder { + to: Address, + chain_id: Option, + block_height: Option, + nonce: Option, + gas_limit: Option, + action: Option, +} + +#[allow(missing_docs)] +impl AutomationRecordBuilder { + /// New builder with the target address as input. + pub fn new(to: Address) -> Self { + Self { + to, + chain_id: None, + block_height: None, + nonce: None, + gas_limit: None, + action: None, + } + } + pub fn with_block_height(mut self, block_height: u64) -> Self { + self.block_height = Some(block_height); + self + } + + pub fn with_nonce(mut self, nonce: u64) -> Self { + self.nonce = Some(nonce); + self + } + + pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + pub fn process_task_indexes(mut self, cycle_index: u64, task_indexes: Vec) -> Self { + self.action = Some(AutomationRecordAction::process(cycle_index, task_indexes)); + self + } + + pub fn remove_task(mut self, cycle_index: u64, task_index: u64, reason: String) -> Self { + self.action = Some(AutomationRecordAction::remove( + cycle_index, + task_index, + reason, + )); + self + } + + pub fn with_chain_id(mut self, chain_id: ChainId) -> Self { + self.chain_id = Some(chain_id); + self + } + + /// Returns the action set on this builder so far, if any. + pub fn action(&self) -> &Option { + &self.action + } + + pub fn build(self) -> Result { + let Self { + to, + chain_id, + block_height, + nonce, + gas_limit, + action, + } = self; + let block_height = value_or_error!(AutomationRecordBuilder, "block_height", block_height); + let nonce = value_or_error!(AutomationRecordBuilder, "nonce", nonce); + let gas_limit = value_or_error!(AutomationRecordBuilder, "gas_limit", gas_limit); + let chain_id = value_or_error!(AutomationRecordBuilder, "chain_id", chain_id); + let action = value_or_error!(AutomationRecordBuilder, "action", action); + let input = action.into_bytes(); + + Ok(AutomationRegistryRecord { + sender: VM_SIGNER, + chain_id, + block_height, + nonce, + gas_limit, + to, + input, + }) + } + + pub fn task_count(&self) -> usize { + self.action.as_ref().map(|a| a.task_count()).unwrap_or(0) + } + + pub fn flatten(mut self) -> Vec { + let Some(action) = self.action.take() else { + return vec![self]; + }; + let builder_base = self.clone(); + action + .flatten() + .into_iter() + .map(|a| { + let mut b = builder_base.clone(); + b.action = Some(a); + b.nonce = None; + b + }) + .collect() + } + + pub fn task_range(&self) -> (u64, u64) { + self.action + .as_ref() + .map(|action| action.task_range()) + .unwrap_or_else(|| (u64::MAX, u64::MAX)) + } + + pub fn into_task_indexes(self) -> Vec { + self.action + .map(|action| action.into_task_indexes()) + .unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::address; + use alloy_consensus::transaction::Transaction; + use primitives::supra_constants::VM_SIGNER; + + const REGISTRY_ADDR: Address = address!("0000000000000000000000000000000000001234"); + const CHAIN_ID: ChainId = 6; + const BLOCK_HEIGHT: u64 = 100; + const NONCE: u64 = 3; + const GAS_LIMIT: u64 = 500_000; + const CYCLE_INDEX: u64 = 42; + + fn get_process_tasks_payload(_cycle_index: u64, _task_indexes: Vec) -> Bytes { + let process_task_call = processTasksCall { + _cycleIndex: _cycle_index, + _taskIndexes: _task_indexes.into_iter().map(U256::from).collect(), + }; + Bytes::from(process_task_call.abi_encode()) + } + + fn base_builder() -> AutomationRecordBuilder { + AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_chain_id(CHAIN_ID) + .with_block_height(BLOCK_HEIGHT) + .with_nonce(NONCE) + .with_gas_limit(GAS_LIMIT) + } + + // ── Builder: successful builds ──────────────────────────────────────────── + + #[test] + fn build_process_record_sets_all_fields() { + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, vec![1, 2, 3]) + .build() + .unwrap(); + + assert_eq!(record.sender, VM_SIGNER); + assert_eq!(record.chain_id, CHAIN_ID); + assert_eq!(record.block_height, BLOCK_HEIGHT); + assert_eq!(record.nonce, NONCE); + assert_eq!(record.gas_limit, GAS_LIMIT); + assert_eq!(record.to, REGISTRY_ADDR); + assert!(!record.input.is_empty()); + } + + #[test] + fn build_remove_record_sets_all_fields() { + let record = base_builder() + .remove_task(CYCLE_INDEX, 7, "expired".to_string()) + .build() + .unwrap(); + + assert_eq!(record.sender, VM_SIGNER); + assert_eq!(record.chain_id, CHAIN_ID); + assert_eq!(record.block_height, BLOCK_HEIGHT); + assert_eq!(record.nonce, NONCE); + assert_eq!(record.gas_limit, GAS_LIMIT); + assert_eq!(record.to, REGISTRY_ADDR); + assert!(!record.input.is_empty()); + } + + #[test] + fn build_process_record_with_empty_task_list() { + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, vec![]) + .build() + .unwrap(); + assert!(!record.input.is_empty()); // selector + ABI-encoded empty array still produces bytes + } + + // ── Builder: missing mandatory field errors ─────────────────────────────── + + #[test] + fn build_missing_block_height_returns_error() { + let err = AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_chain_id(CHAIN_ID) + .with_nonce(NONCE) + .with_gas_limit(GAS_LIMIT) + .process_task_indexes(CYCLE_INDEX, vec![1]) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "block_height") + ); + } + + #[test] + fn build_missing_nonce_returns_error() { + let err = AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_chain_id(CHAIN_ID) + .with_block_height(BLOCK_HEIGHT) + .with_gas_limit(GAS_LIMIT) + .process_task_indexes(CYCLE_INDEX, vec![1]) + .build() + .unwrap_err(); + assert!(matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "nonce")); + } + + #[test] + fn build_missing_gas_limit_returns_error() { + let err = AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_chain_id(CHAIN_ID) + .with_block_height(BLOCK_HEIGHT) + .with_nonce(NONCE) + .process_task_indexes(CYCLE_INDEX, vec![1]) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "gas_limit") + ); + } + + #[test] + fn build_missing_chain_id_returns_error() { + let err = AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_block_height(BLOCK_HEIGHT) + .with_nonce(NONCE) + .with_gas_limit(GAS_LIMIT) + .process_task_indexes(CYCLE_INDEX, vec![1]) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "chain_id") + ); + } + + #[test] + fn build_missing_action_returns_error() { + let err = AutomationRecordBuilder::new(REGISTRY_ADDR) + .with_chain_id(CHAIN_ID) + .with_block_height(BLOCK_HEIGHT) + .with_nonce(NONCE) + .with_gas_limit(GAS_LIMIT) + .build() + .unwrap_err(); + assert!(matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "action")); + } + + // ── Transaction trait impl ──────────────────────────────────────────────── + + #[test] + fn transaction_trait_field_accessors() { + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, vec![5]) + .build() + .unwrap(); + + assert_eq!(record.chain_id(), Some(CHAIN_ID)); + assert_eq!(record.nonce(), NONCE); + assert_eq!(record.gas_limit(), GAS_LIMIT); + assert_eq!(record.gas_price(), None); + assert_eq!(record.max_fee_per_gas(), 0); + assert_eq!(record.max_priority_fee_per_gas(), Some(0)); + assert_eq!(record.max_fee_per_blob_gas(), None); + assert_eq!(record.priority_fee_or_price(), 0); + assert_eq!(record.effective_gas_price(None), 0); + assert_eq!(record.effective_gas_price(Some(100)), 0); + assert!(!record.is_dynamic_fee()); + assert_eq!(record.kind(), TxKind::Call(REGISTRY_ADDR)); + assert!(!record.is_create()); + assert_eq!(record.value(), U256::ZERO); + assert_eq!(record.access_list(), None); + assert_eq!(record.blob_versioned_hashes(), None); + assert_eq!(record.authorization_list(), None); + } + + #[test] + fn transaction_input_matches_built_payload() { + let task_indexes = vec![10u64, 20]; + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, task_indexes.clone()) + .build() + .unwrap(); + + let expected = get_process_tasks_payload(CYCLE_INDEX, task_indexes); + assert_eq!(record.input(), &expected); + } + + // ── try_convert_to_action ───────────────────────────────────────────────── + + #[test] + fn convert_process_input_roundtrips() { + let task_indexes = vec![1u64, 2, 3]; + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, task_indexes.clone()) + .build() + .unwrap(); + + let action = record.try_convert_to_action().unwrap(); + let AutomationRecordAction::Process(process) = action else { + panic!("Expected Process action, got {action:?}"); + }; + assert_eq!(process._cycleIndex, CYCLE_INDEX); + assert_eq!( + process._taskIndexes, + task_indexes.into_iter().map(U256::from).collect::>() + ); + } + + #[test] + fn convert_remove_input_roundtrips() { + let record = base_builder() + .remove_task(CYCLE_INDEX, 99, "bad task".to_string()) + .build() + .unwrap(); + + let action = record.try_convert_to_action().unwrap(); + let AutomationRecordAction::Remove(remove) = action else { + panic!("Expected Remove action, got {action:?}"); + }; + assert_eq!(remove._cycleIndex, CYCLE_INDEX); + assert_eq!(remove._taskIndex, 99); + assert_eq!(remove._reason, "bad task".to_string()); + } + + #[test] + fn convert_empty_input_returns_error() { + let record = AutomationRegistryRecord { + input: Bytes::default(), + ..Default::default() + }; + assert!(matches!( + record.try_convert_to_action(), + Err(SupraExtensionError::InvalidAutomationRecord(_)) + )); + } + + #[test] + fn convert_short_input_returns_error() { + let record = AutomationRegistryRecord { + input: Bytes::from(vec![0xAB, 0xCD]), + ..Default::default() + }; + assert!(matches!( + record.try_convert_to_action(), + Err(SupraExtensionError::InvalidAutomationRecord(_)) + )); + } + + #[test] + fn convert_unknown_selector_returns_error() { + let record = AutomationRegistryRecord { + input: Bytes::from(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00]), + ..Default::default() + }; + assert!(matches!( + record.try_convert_to_action(), + Err(SupraExtensionError::InvalidAutomationRecord(_)) + )); + } + + // ── try_get_action_tag ──────────────────────────────────────────────────── + + #[test] + fn get_action_tag_process() { + let record = base_builder() + .process_task_indexes(CYCLE_INDEX, vec![1]) + .build() + .unwrap(); + assert_eq!( + record.try_get_action_tag().unwrap(), + AutomationRecordActionTag::Process + ); + } + + #[test] + fn get_action_tag_remove() { + let record = base_builder() + .remove_task(CYCLE_INDEX, 5, "reason".to_string()) + .build() + .unwrap(); + assert_eq!( + record.try_get_action_tag().unwrap(), + AutomationRecordActionTag::Remove + ); + } + + #[test] + fn get_action_tag_empty_input_returns_error() { + let record = AutomationRegistryRecord { + input: Bytes::default(), + ..Default::default() + }; + assert!(matches!( + record.try_get_action_tag(), + Err(SupraExtensionError::InvalidAutomationRecord(_)) + )); + } + + #[test] + fn get_action_tag_unknown_selector_returns_error() { + let record = AutomationRegistryRecord { + input: Bytes::from(vec![0xFF, 0xFF, 0xFF, 0xFF]), + ..Default::default() + }; + assert!(matches!( + record.try_get_action_tag(), + Err(SupraExtensionError::InvalidAutomationRecord(_)) + )); + } + + // ── AutomationRecordAction methods ──────────────────────────────────────── + + #[test] + fn action_into_task_indexes_process() { + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 0, + _taskIndexes: vec![U256::from(10), U256::from(20), U256::from(30)], + }); + assert_eq!(action.into_task_indexes(), vec![10, 20, 30]); + } + + #[test] + fn action_into_task_indexes_remove() { + let action = AutomationRecordAction::Remove(removeRegisteredTaskCall { + _taskIndex: 7, + _reason: String::new(), + _cycleIndex: 8, + }); + assert_eq!(action.into_task_indexes(), vec![7]); + } + + #[test] + fn action_task_count() { + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 0, + _taskIndexes: vec![U256::from(10), U256::from(20), U256::from(30)], + }); + assert_eq!(action.task_count(), 3); + + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 0, + _taskIndexes: vec![], + }); + assert_eq!(action.task_count(), 0); + assert_eq!( + AutomationRecordAction::Remove(removeRegisteredTaskCall { + _cycleIndex: 4, + _taskIndex: 2, + _reason: "".to_string(), + }) + .task_count(), + 1 + ); + } + + #[test] + fn action_flatten_process_produces_single_task_actions() { + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 2, + _taskIndexes: vec![U256::from(1), U256::from(2), U256::from(3)], + }); + let flat = action.flatten(); + assert_eq!(flat.len(), 3); + flat.into_iter().enumerate().for_each(|(idx, item)| { + let AutomationRecordAction::Process(process) = item else { + panic!("Expected Process action, got {item:?}"); + }; + assert_eq!(process._cycleIndex, 2); + assert_eq!(process._taskIndexes, vec![U256::from(idx + 1)]); + }); + } + + #[test] + fn action_flatten_remove_stays_single() { + let action = AutomationRecordAction::Remove(removeRegisteredTaskCall { + _taskIndex: 5, + _reason: "x".to_string(), + _cycleIndex: 7, + }); + let flat = action.clone().flatten(); + assert_eq!(flat.len(), 1); + assert_eq!(flat[0], action); + } + + #[test] + fn action_task_range_process() { + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 0, + _taskIndexes: vec![U256::from(3), U256::from(1), U256::from(4), U256::from(5)], + }); + assert_eq!(action.task_range(), (1, 5)); + } + + #[test] + fn action_task_range_empty_process_returns_max() { + let action = AutomationRecordAction::Process(processTasksCall { + _cycleIndex: 0, + _taskIndexes: vec![], + }); + assert_eq!(action.task_range(), (u64::MAX, u64::MAX)); + } + + #[test] + fn action_task_range_remove() { + let action = AutomationRecordAction::Remove(removeRegisteredTaskCall { + _taskIndex: 42, + _reason: String::new(), + _cycleIndex: 3, + }); + assert_eq!(action.task_range(), (42, 42)); + } + + // ── AutomationRecordBuilder utility methods ─────────────────────────────── + + #[test] + fn builder_task_count_no_action() { + let builder = AutomationRecordBuilder::new(REGISTRY_ADDR); + assert_eq!(builder.task_count(), 0); + } + + #[test] + fn builder_task_count_with_action() { + let builder = base_builder().process_task_indexes(CYCLE_INDEX, vec![1, 2, 3]); + assert_eq!(builder.task_count(), 3); + } + + #[test] + fn builder_task_range_no_action() { + let builder = AutomationRecordBuilder::new(REGISTRY_ADDR); + assert_eq!(builder.task_range(), (u64::MAX, u64::MAX)); + } + + #[test] + fn builder_task_range_with_action() { + let builder = base_builder().process_task_indexes(CYCLE_INDEX, vec![5, 2, 8]); + assert_eq!(builder.task_range(), (2, 8)); + } + + #[test] + fn builder_into_task_indexes() { + let builder = base_builder().process_task_indexes(CYCLE_INDEX, vec![7, 8, 9]); + assert_eq!(builder.into_task_indexes(), vec![7, 8, 9]); + } + + #[test] + fn builder_into_task_indexes_no_action() { + let builder = AutomationRecordBuilder::new(REGISTRY_ADDR); + assert_eq!(builder.into_task_indexes(), Vec::::new()); + } + + #[test] + fn builder_flatten_clears_nonce_on_each_part() { + let builder = base_builder().process_task_indexes(CYCLE_INDEX, vec![1, 2, 3]); + let parts = builder.flatten(); + assert_eq!(parts.len(), 3); + for part in &parts { + assert!(part.action.is_some()); + assert!(part.nonce.is_none(), "nonce must be cleared after flatten"); + } + } + + #[test] + fn builder_flatten_remove_is_single_and_keeps_nonce_cleared() { + let builder = base_builder().remove_task(CYCLE_INDEX, 10, "r".to_string()); + let parts = builder.flatten(); + assert_eq!(parts.len(), 1); + assert!(parts[0].nonce.is_none()); + } + + #[test] + fn builder_flatten_no_action_returns_self() { + let builder = base_builder(); + let parts = builder.flatten(); + assert_eq!(parts.len(), 1); + } +} diff --git a/crates/supra-extension/src/transactions/block_metadata.rs b/crates/supra-extension/src/transactions/block_metadata.rs new file mode 100644 index 0000000000..31633d336b --- /dev/null +++ b/crates/supra-extension/src/transactions/block_metadata.rs @@ -0,0 +1,380 @@ +//! Definition of the block metadata transaction which will be executed for every block +//! to aid block based checks to assist chain regular operations + +use crate::blockPrologueCall; +use crate::errors::SupraExtensionError; +use crate::value_or_error; +use alloy::primitives::{Address, Bytes, ChainId, B256, U256}; +use alloy_consensus::transaction::Transaction; +use alloy_eips::eip2718::Typed2718; +use alloy_sol_types::SolCall; +use context::transaction::{AccessList, SignedAuthorization}; +use context::TransactionType; +use primitives::eip7825::TX_GAS_LIMIT_CAP; +use primitives::supra_constants::VM_SIGNER; +use primitives::TxKind; + +/// Default Gas limit used for the block metadata transaction. +pub const DEFAULT_BLOCK_METADATA_GAS_LIMIT: u64 = TX_GAS_LIMIT_CAP; + +/// EVM system transaction generated based on the block sent for execution. +/// Will trigger `BlockMeta::block_prologue` supra-evm SC API execution to meet +/// other `supra-evm` SC checks requiring per-block execution. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct BlockMetadata { + /// ID of the chain in scope of which block is being executed + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub chain_id: ChainId, + /// Sender of the transaction. By default, will be agreed @evm_vm_signer + pub sender: Address, + /// A height of the block based on which this transaction is created + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub height: u64, + /// Hash of the block being executed + pub block_hash: B256, + /// Block creation timestamp in seconds + pub timestamp: U256, + /// The 160-bit address of the message call’s recipient + #[cfg_attr(feature = "serde", serde(default))] + pub to: Address, + /// An unlimited size byte array specifying the + /// input data of the message call. + pub input: Bytes, + /// Gas limit for this transaction, resolved from `BlockMeta.blockPrologueGasCap`. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub gas_limit: u64, +} + +impl Transaction for BlockMetadata { + #[inline] + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + #[inline] + fn nonce(&self) -> u64 { + self.height + } + + #[inline] + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + #[inline] + fn gas_price(&self) -> Option { + None + } + + #[inline] + fn max_fee_per_gas(&self) -> u128 { + 0 + } + + #[inline] + fn max_priority_fee_per_gas(&self) -> Option { + Some(0) + } + + #[inline] + fn max_fee_per_blob_gas(&self) -> Option { + None + } + + #[inline] + fn priority_fee_or_price(&self) -> u128 { + 0 + } + + fn effective_gas_price(&self, _base_fee: Option) -> u128 { + 0 + } + + #[inline] + fn is_dynamic_fee(&self) -> bool { + false + } + + #[inline] + fn kind(&self) -> TxKind { + TxKind::Call(self.to) + } + + #[inline] + fn is_create(&self) -> bool { + false + } + + #[inline] + fn value(&self) -> U256 { + U256::from(0) + } + + #[inline] + fn input(&self) -> &Bytes { + &self.input + } + + #[inline] + fn access_list(&self) -> Option<&AccessList> { + None + } + + #[inline] + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + None + } + + #[inline] + fn authorization_list(&self) -> Option<&[SignedAuthorization]> { + None + } +} +impl Typed2718 for BlockMetadata { + fn ty(&self) -> u8 { + TransactionType::Custom as u8 + } +} + +/// Builder for [`BlockMetadata`] transaction. +/// All properties are mandatory. +#[derive(Clone, Debug, Default)] +pub struct BlockMetadataBuilder { + to: Address, + height: Option, + block_hash: Option, + timestamp: Option, + chain_id: Option, + gas_limit: Option, +} + +#[allow(missing_docs)] +impl BlockMetadataBuilder { + pub fn new(to: Address) -> Self { + Self { + to, + height: None, + block_hash: None, + timestamp: None, + chain_id: None, + gas_limit: None, + } + } + + /// Address this transaction was built to target, i.e. the `BlockMeta` contract address. + pub fn to(&self) -> Address { + self.to + } + + pub fn height(mut self, height: u64) -> Self { + self.height = Some(height); + self + } + + pub fn block_hash(mut self, block_hash: B256) -> Self { + self.block_hash = Some(block_hash); + self + } + pub fn chain_id(mut self, chain_id: u64) -> Self { + self.chain_id = Some(chain_id); + self + } + + pub fn timestamp(mut self, timestamp: U256) -> Self { + self.timestamp = Some(timestamp); + self + } + + pub fn gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + pub fn build(self) -> Result { + let Self { + to, + height, + block_hash, + timestamp, + chain_id, + gas_limit, + } = self; + let height = value_or_error!(BlockMetadataBuilder, "height", height); + let block_hash = value_or_error!(BlockMetadataBuilder, "block_hash", block_hash); + let timestamp = value_or_error!(BlockMetadataBuilder, "timestamp", timestamp); + let chain_id = value_or_error!(BlockMetadataBuilder, "chain_id", chain_id); + let gas_limit = value_or_error!(BlockMetadataBuilder, "gas_limit", gas_limit); + + Ok(BlockMetadata { + chain_id, + sender: VM_SIGNER, + height, + block_hash, + timestamp, + to, + input: Self::get_block_prologue(), + gas_limit, + }) + } + + fn get_block_prologue() -> Bytes { + Bytes::from(blockPrologueCall.abi_encode()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::SupraExtensionError; + use alloy::primitives::{address, b256, Address, B256, U256}; + use alloy_consensus::transaction::Transaction; + use alloy_eips::eip2718::Typed2718; + use primitives::supra_constants::VM_SIGNER; + + const REGISTRY: Address = address!("1111111111111111111111111111111111111111"); + const CHAIN_ID: u64 = 6; + const HEIGHT: u64 = 42; + const BLOCK_HASH: B256 = + b256!("abababababababababababababababababababababababababababababababab"); + const TIMESTAMP: u64 = 1_700_000_000; + const GAS_LIMIT: u64 = 12_345_678; + + fn full_builder() -> BlockMetadataBuilder { + BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .block_hash(BLOCK_HASH) + .timestamp(U256::from(TIMESTAMP)) + .chain_id(CHAIN_ID) + .gas_limit(GAS_LIMIT) + } + + // ── Builder: successful build ───────────────────────────────────────────── + + #[test] + fn build_sets_all_fields_correctly() { + let meta = full_builder().build().unwrap(); + + assert_eq!(meta.sender, VM_SIGNER); + assert_eq!(meta.chain_id, CHAIN_ID); + assert_eq!(meta.height, HEIGHT); + assert_eq!(meta.block_hash, BLOCK_HASH); + assert_eq!(meta.timestamp, U256::from(TIMESTAMP)); + assert_eq!(meta.to, REGISTRY); + assert_eq!(meta.gas_limit, GAS_LIMIT); + } + + #[test] + fn builder_to_returns_target_address() { + assert_eq!(BlockMetadataBuilder::new(REGISTRY).to(), REGISTRY); + } + + #[test] + fn build_input_matches_block_prologue_abi_encoding() { + use crate::blockPrologueCall; + use alloy_sol_types::SolCall; + let meta = full_builder().build().unwrap(); + assert_eq!( + meta.input.as_ref(), + blockPrologueCall.abi_encode().as_slice() + ); + } + + // ── Builder: missing mandatory field errors ─────────────────────────────── + + #[test] + fn build_missing_mandatory_field_returns_error() { + let err = BlockMetadataBuilder::new(REGISTRY) + .block_hash(BLOCK_HASH) + .timestamp(U256::from(TIMESTAMP)) + .chain_id(CHAIN_ID) + .build() + .unwrap_err(); + assert!(matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "height")); + + let err = BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .timestamp(U256::from(TIMESTAMP)) + .chain_id(CHAIN_ID) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "block_hash") + ); + + let err = BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .block_hash(BLOCK_HASH) + .chain_id(CHAIN_ID) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "timestamp") + ); + + let err = BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .block_hash(BLOCK_HASH) + .timestamp(U256::from(TIMESTAMP)) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "chain_id") + ); + + let err = BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .block_hash(BLOCK_HASH) + .timestamp(U256::from(TIMESTAMP)) + .chain_id(CHAIN_ID) + .build() + .unwrap_err(); + assert!( + matches!(err, SupraExtensionError::MissingBuilderValue(_, ref f) if f == "gas_limit") + ); + } + + // ── Transaction trait impl ──────────────────────────────────────────────── + + #[test] + fn transaction_trait_field_accessors() { + let meta = full_builder().build().unwrap(); + + assert_eq!(meta.chain_id(), Some(CHAIN_ID)); + assert_eq!(meta.nonce(), HEIGHT); // nonce == height + assert_eq!(meta.gas_limit(), GAS_LIMIT); + assert_eq!(meta.gas_price(), None); + assert_eq!(meta.max_fee_per_gas(), 0); + assert_eq!(meta.max_priority_fee_per_gas(), Some(0)); + assert_eq!(meta.max_fee_per_blob_gas(), None); + assert_eq!(meta.priority_fee_or_price(), 0); + assert_eq!(meta.effective_gas_price(None), 0); + assert_eq!(meta.effective_gas_price(Some(9_999)), 0); + assert!(!meta.is_dynamic_fee()); + assert_eq!(meta.kind(), TxKind::Call(REGISTRY)); + assert!(!meta.is_create()); + assert_eq!(meta.value(), U256::ZERO); + assert_eq!(meta.access_list(), None); + assert_eq!(meta.blob_versioned_hashes(), None); + assert_eq!(meta.authorization_list(), None); + + assert_eq!(meta.input(), &meta.input); + assert_eq!(meta.ty(), 0xFF); // TransactionType::Custom + } + + // ── BlockMetadata default ───────────────────────────────────────────────── + + #[test] + fn default_produces_zero_values() { + let meta = BlockMetadata::default(); + assert_eq!(meta.chain_id, 0); + assert_eq!(meta.sender, Address::ZERO); + assert_eq!(meta.height, 0); + assert_eq!(meta.block_hash, B256::ZERO); + assert_eq!(meta.timestamp, U256::ZERO); + assert_eq!(meta.to, Address::ZERO); + assert!(meta.input.is_empty()); + assert_eq!(meta.gas_limit, 0); + } +} diff --git a/crates/supra-extension/src/transactions/mod.rs b/crates/supra-extension/src/transactions/mod.rs new file mode 100644 index 0000000000..ef7b8c51d4 --- /dev/null +++ b/crates/supra-extension/src/transactions/mod.rs @@ -0,0 +1,4 @@ +//! Set of transactions introduced as part of the supra extension +pub mod automated_transaction; +pub mod automation_record; +pub mod block_metadata; diff --git a/examples/cheatcode_inspector/src/main.rs b/examples/cheatcode_inspector/src/main.rs index 7337d1ef18..aaaa3e9a74 100644 --- a/examples/cheatcode_inspector/src/main.rs +++ b/examples/cheatcode_inspector/src/main.rs @@ -252,6 +252,10 @@ impl JournalTr for Backend { fn discard_tx(&mut self) { self.journaled_state.discard_tx() } + + fn has_state_mutations(&self) -> bool { + self.journaled_state.has_state_mutations() + } } impl JournalExt for Backend { diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..c8fb1836dc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,14 @@ +# The Rust version this fork builds and tests with. It matches the pin in `smr-moonshot`, the +# consumer of these crates, rather than the workspace `rust-version` (1.88.0) that upstream `revm` +# declares as its MSRV: this fork only ever has to compile under Supra's toolchain, and the range of +# compilers that actually build it is narrower than the MSRV suggests. +# +# This file is the only functional pin: rustup prefers it over `rustup default`, so it also decides +# the toolchain in CI, whatever channel a toolchain-install step there names. The jobs that +# genuinely need a different compiler — the MSRV/nightly test matrix, and the book's nightly +# rustdoc — override it with `RUSTUP_TOOLCHAIN`, which rustup ranks above this file. +[toolchain] +channel = "1.97.1" +components = ["clippy", "rust-docs", "rustfmt"] +# `check-no-std` cross-compiles to this target. +targets = ["riscv32imac-unknown-none-elf"] diff --git a/solidity/supra_contracts/README.md b/solidity/supra_contracts/README.md new file mode 100644 index 0000000000..e894720164 --- /dev/null +++ b/solidity/supra_contracts/README.md @@ -0,0 +1,48 @@ +## Supra EVM Automation Registry + +**This repository includes following smart contracts:** +- MultiSignatureWallet and MultisigBeacon +- ERC20Supra +- BlockMeta +- Automation Registry smart contracts + - AutomationCore: manages configuration, refunds, fee accounting and other helper functions + - AutomationRegistry: user facing contract to register/cancel/stop a task + - AutomationController: manages cycle transition and processing of tasks + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Install dependencies + +``` +forge install OpenZeppelin/openzeppelin-contracts +forge install OpenZeppelin/openzeppelin-contracts-upgradeable +``` + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Deploying Automation Registry smart contracts + +```shell +$ forge script script/DeployAutomationRegistry.s.sol:DeployAutomationRegistry --rpc-url --private-key +``` \ No newline at end of file diff --git a/solidity/supra_contracts/automation_registry.py b/solidity/supra_contracts/automation_registry.py new file mode 100755 index 0000000000..37aecebe00 --- /dev/null +++ b/solidity/supra_contracts/automation_registry.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +""" +Automation Registry CLI +Uses Foundry's `cast` for all on-chain operations. +Run: python automation_registry.py + +Dependencies: + pip install eth-utils +""" + +import json +import os +import readline +import sys +import subprocess +from decimal import Decimal, InvalidOperation +from pathlib import Path +from typing import Callable, Optional +try: + from eth_utils import is_address, to_checksum_address +except ImportError: + print("❌ Missing dependency: eth-utils") + print(" Install with: pip install eth-utils") + sys.exit(1) + +# ───────────────────────────────────────────── +# Environment +# ───────────────────────────────────────────── + +ENV: dict[str, str] = {} + + +def load_env_file(path: str) -> dict[str, str]: + """Parse a simple KEY=VALUE .env file (no shell substitution).""" + result: dict[str, str] = {} + p = Path(path) + if not p.exists(): + return result + for raw_line in p.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + result[key.strip()] = value.strip().strip("'\"") + return result + + +def load_environments() -> None: + """Load .env and deployed.env, validate required keys.""" + global ENV + ENV = {**load_env_file(".env"), **load_env_file("deployed.env")} + for k, v in ENV.items(): + os.environ.setdefault(k, v) + required = ["RPC_URL", "ERC20_SUPRA", "ERC20_SUPRA_HANDLER", "DIAMOND"] + missing = [k for k in required if not ENV.get(k)] + if missing: + print(f"❌ Missing required environment variables: {', '.join(missing)}") + print(" Check .env and deployed.env") + sys.exit(1) + + +def cfg(key: str) -> str: + return ENV[key] + + +# ───────────────────────────────────────────── +# Cast helpers +# ───────────────────────────────────────────── + +def run_cast(args: list[str]) -> str: + """Run a cast command; return stripped stdout. Raises RuntimeError on failure.""" + result = subprocess.run(["cast"] + args, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or result.stdout.strip()) + return result.stdout.strip() + + +def cast_call(contract: str, sig: str, *args: str) -> str: + return run_cast(["call", contract, sig, *args, "--rpc-url", cfg("RPC_URL")]) + + +def send_tx(account: str, contract: str, sig: str, *tx_args: str, value_wei: Optional[str] = None) -> None: + """ + Send a transaction via the Foundry keystore account. + cast handles the password prompt and prints tx output directly to the terminal. + Pass value_wei to attach native SUPRA to the call. + """ + cmd = [ + "cast", "send", + "--rpc-url", cfg("RPC_URL"), + "--account", account, + "--gas-limit", "3000000", + ] + if value_wei is not None: + cmd += ["--value", value_wei] + cmd += [contract, sig, *tx_args] + # capture_output intentionally omitted: cast needs the terminal for keystore password. + result = subprocess.run(cmd) + if result.returncode != 0: + print("❌ Transaction failed.") + + +# ───────────────────────────────────────────── +# Validation +# ───────────────────────────────────────────── +# +# Input types and their validators: +# +# validate_address(raw, label) +# Ethereum address — delegates to eth_utils for checksum + format. +# +# validate_decimal_amount(raw, label, *, unit) +# Positive decimal in SUPRA (unit="ether") or GWEI (unit="gwei"). +# Used for: deposit amount, approval amount, automation fee cap, gas price cap. +# Returns the wei string produced by cast. +# +# validate_int(raw, label, *, min_value) +# Integer with a configurable floor. +# min_value=1 → positive integers only (maxGasAmount, duration in seconds) +# min_value=0 → non-negative integers (priority, task indexes) +# +# validate_bytes_hex(raw, label) +# 0x-prefixed hex string for ABI-encoded bytes payloads. +# +# validate_index_array(raw) +# Comma-separated or bracketed list of non-negative task indexes. + +import re as _re +_DECIMAL_RE = _re.compile(r"^[0-9]+(\.[0-9]+)?$") +_HEX_RE = _re.compile(r"^0x[a-fA-F0-9]*$") + + +def validate_address(raw: str, label: str = "Address") -> str: + """Validate and return a checksummed Ethereum address.""" + s = raw.strip() + if not s: + raise ValueError(f"{label} cannot be empty.") + if not is_address(s): + raise ValueError(f"{label} '{s}' is not a valid Ethereum address.") + return to_checksum_address(s) + + +def validate_decimal_amount(raw: str, label: str, *, unit: str) -> str: + """ + Validate a positive decimal string and convert to wei via cast. + + unit="ether" → SUPRA amounts (deposit, approval, automation fee cap) + unit="gwei" → gas price cap + + Returns the wei string. Raises ValueError on any invalid input. + """ + s = raw.strip() + if not s: + raise ValueError(f"{label} cannot be empty.") + if not _DECIMAL_RE.match(s): + raise ValueError( + f"{label} '{s}' is invalid. " + "Use a plain positive number, e.g. 1 or 0.5. " + "No scientific notation or leading/trailing dot." + ) + try: + if Decimal(s) <= 0: + raise ValueError(f"{label} must be greater than zero.") + except InvalidOperation: + raise ValueError(f"{label} '{s}' could not be parsed.") + try: + wei = run_cast(["--to-wei", s] + ([unit] if unit != "ether" else [])) + except RuntimeError as e: + raise ValueError(f"cast conversion failed: {e}") + if not wei or wei == "0": + raise ValueError(f"{label} is too small (rounds to 0 wei).") + return wei + + +def validate_int(raw: str, label: str, *, min_value: int) -> int: + """ + Validate an integer with a configurable minimum. + + min_value=1 → strictly positive (maxGasAmount, duration in seconds) + min_value=0 → non-negative (priority, task indexes) + """ + s = raw.strip() + if not s.isdigit(): + bound = "positive integer (> 0)" if min_value > 0 else "non-negative integer (>= 0)" + raise ValueError(f"{label} must be a {bound}, got '{s}'.") + value = int(s) + if value < min_value: + raise ValueError(f"{label} must be >= {min_value}, got {value}.") + return value + +def validate_bytes_hex(raw: str, label: str = "Bytes", *, allow_empty: bool = False) -> str: + """Validate a 0x-prefixed hex string (ABI-encoded bytes payload). + + - Rejects bare '0x' unless allow_empty=True + - Rejects odd-length hex body (not valid ABI bytes encoding) + """ + s = raw.strip() + if not _HEX_RE.match(s): + raise ValueError(f"{label} must be a 0x-prefixed hex string, got '{s}'.") + hex_body = s[2:] + if not allow_empty and len(hex_body) == 0: + raise ValueError(f"{label} must not be empty (bare '0x' is not valid).") + if len(hex_body) % 2 != 0: + raise ValueError( + f"{label} has an odd number of hex characters ({len(hex_body)}). " + "Each byte requires exactly 2 hex characters." + ) + return s + +def validate_index_array(raw: str) -> list[int]: + """ + Parse a task index list in any of these forms: + '4' → [4] + '1,2,3' → [1, 2, 3] + '[1,2,3]' → [1, 2, 3] + Every element must be >= 0. + """ + s = raw.strip().strip("[]").strip() + if not s: + raise ValueError("Task index list cannot be empty.") + indexes: list[int] = [] + for part in s.split(","): + p = part.strip() + if not p.isdigit(): + raise ValueError(f"Invalid task index '{p}'. Must be a non-negative integer.") + indexes.append(int(p)) + return indexes + + +def fmt_index_array(indexes: list[int]) -> str: + return "[" + ",".join(str(i) for i in indexes) + "]" + + +# ───────────────────────────────────────────── +# Prompt helpers +# ───────────────────────────────────────────── + +def prompt(label: str) -> str: + try: + return input(f" {label}: ").strip() + except EOFError: + return "" + + +def prompt_validated(label: str, validator, *args, **kwargs): + """Loop, re-prompting on validation error, until validator succeeds.""" + while True: + raw = prompt(label) + try: + return validator(raw, *args, **kwargs) + except ValueError as e: + print(f" ⚠ {e}") + + +_ACCOUNT_NAME_RE = _re.compile(r"^[a-zA-Z0-9_\-]+$") + +def get_keystore_account() -> str: + """Prompt for a Foundry keystore account name and verify the file exists on disk.""" + keystore_dir = Path.home() / ".foundry" / "keystores" + while True: + account = prompt("Keystore account name") + if not account: + print(" ⚠ Account name cannot be empty.") + continue + if not _ACCOUNT_NAME_RE.match(account): + print(" ⚠ Account name may only contain letters, digits, hyphens, and underscores.") + continue + if not (keystore_dir / account).exists(): + print(f" ❌ Account '{account}' not found in {keystore_dir}") + print(" Available accounts:") + try: + for line in run_cast(["wallet", "list"]).splitlines(): + print(f" - {line}") + except RuntimeError: + print(" (could not list accounts)") + continue + return account + + +# ───────────────────────────────────────────── +# Shared internal utilities +# ───────────────────────────────────────────── + +def _first_token(raw: str) -> str: + """Return the first whitespace-separated token from cast output (the wei integer).""" + return raw.split()[0] if raw else "0" + + +def _print_supra(label: str, wei_str: str) -> None: + print(f" {label}: {run_cast(['--from-wei', wei_str])} SUPRA") + + +def _cancel_or_stop(method_sig: str) -> None: + """Shared flow for cancel/stop commands: collect indexes, get account, send tx.""" + indexes = prompt_validated( + "Task index(es) (e.g. 4 or 1,2,3 or [0,1,2,3])", + validate_index_array, + ) + account = get_keystore_account() + send_tx(account, cfg("DIAMOND"), method_sig, fmt_index_array(indexes)) + + +# ───────────────────────────────────────────── +# Commands — read / view +# ───────────────────────────────────────────── + +def cmd_list_accounts() -> None: + print("\n=== Available Keystore Accounts ===") + try: + output = run_cast(["wallet", "list"]) + if output: + for line in output.splitlines(): + print(f" - {line}") + else: + print(" No accounts found.") + print(" Import one with: cast wallet import --interactive") + except RuntimeError as e: + print(f" ❌ {e}") + print() + + +def cmd_native_balance() -> None: + address = prompt_validated("Address", validate_address) + try: + raw = run_cast(["balance", address, "--rpc-url", cfg("RPC_URL")]) or "0" + _print_supra("SUPRA Balance", raw) + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_erc20_supra_balance() -> None: + address = prompt_validated("Address", validate_address) + try: + raw = run_cast(["balance", "--erc20", cfg("ERC20_SUPRA"), address, "--rpc-url", cfg("RPC_URL")]) + _print_supra("ERC20Supra Balance", _first_token(raw)) + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_allowance() -> None: + address = prompt_validated("Address", validate_address) + try: + raw = cast_call( + cfg("ERC20_SUPRA"), "allowance(address,address)(uint256)", + address, cfg("DIAMOND"), + ) + _print_supra("Allowance to Automation Registry", _first_token(raw)) + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_is_submitter() -> None: + address = prompt_validated("Address", validate_address) + try: + raw = cast_call(cfg("DIAMOND"), "isAuthorizedSubmitter(address)(bool)", address) + print(f" Is authorized submitter: {raw}") + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_view_task_details() -> None: + index = prompt_validated("Task index", validate_int, "Task index", min_value=0) + print("\n=== Task Details ===") + try: + raw = run_cast([ + "call", cfg("DIAMOND"), + "getTaskDetails(uint64)((uint128,uint128,uint128,uint128,bytes32," + "uint64,uint64,uint64,uint64,address,uint8,uint8,bytes,bytes,bytes[]))", + str(index), "--rpc-url", cfg("RPC_URL"), "--json", + ]) + if not raw or raw == "null": + print(f" ❌ Task {index} does not exist.") + return + t = json.loads(raw)[0] + task_types = {0: "UST", 1: "GST"} + task_states = {0: "PENDING", 1: "ACTIVE", 2: "CANCELLED"} + fmt_supra = lambda v: f"{Decimal(str(v)) / Decimal('1e18'):.6f} SUPRA" + fmt_gwei = lambda v: f"{Decimal(str(v)) / Decimal('1e9'):.6f} Gwei" + rows = [ + ("maxGasAmount", str(t[0])), + ("gasPriceCap", fmt_gwei(t[1])), + ("automationFeeCapForCycle", fmt_supra(t[2])), + ("depositFee", fmt_supra(t[3])), + ("txHash", t[4]), + ("taskIndex", str(t[5])), + ("registrationTime", str(t[6])), + ("expiryTime", str(t[7])), + ("priority", str(t[8])), + ("owner", t[9]), + ("taskType", task_types.get(t[10], "UNKNOWN")), + ("taskState", task_states.get(t[11], "UNKNOWN")), + ("payloadTx", t[12]), + ("predicate", t[13]), + ("auxData", str(t[14])), + ] + col = max(len(k) for k, _ in rows) + for k, v in rows: + print(f" {k:<{col}} {v}") + except RuntimeError as e: + print(f" ❌ {e}") + print() + + +def cmd_registry_locked_balance() -> None: + try: + raw = cast_call(cfg("DIAMOND"), "getTotalLockedBalance()(uint256)") + _print_supra("Registry Locked SUPRA", _first_token(raw)) + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_registry_balance() -> None: + try: + raw = run_cast(["balance", "--erc20", cfg("ERC20_SUPRA"), cfg("DIAMOND"), "--rpc-url", cfg("RPC_URL")]) + _print_supra("Automation Registry ERC20Supra Balance", _first_token(raw)) + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_task_list() -> None: + try: + raw = cast_call(cfg("DIAMOND"), "getTaskIdList()(uint256[])") + print(f"\n=== Task IDs ===\n {raw}\n") + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_total_tasks() -> None: + try: + raw = cast_call(cfg("DIAMOND"), "totalTasks()(uint256)") + print(f" Total Task Count: {raw}") + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_user_tasks() -> None: + address = prompt_validated("User address", validate_address) + try: + raw = cast_call(cfg("DIAMOND"), "getTasksByAddress(address)(uint256[])", address) + print(f"\n=== User Task IDs ===\n {raw}\n") + except RuntimeError as e: + print(f" ❌ {e}") + + +def cmd_task_exists() -> None: + index = prompt_validated("Task index", validate_int, "Task index", min_value=0) + try: + raw = cast_call(cfg("DIAMOND"), "ifTaskExists(uint64)(bool)", str(index)) + if raw.lower() == "true": + print(f" ✅ Task {index} EXISTS") + else: + print(f" ❌ Task {index} does NOT exist") + except RuntimeError as e: + print(f" ❌ {e}") + print() + + +# ───────────────────────────────────────────── +# Commands — write / send +# ───────────────────────────────────────────── + +def cmd_native_to_erc20() -> None: + print("Deposit native SUPRA → mint ERC20Supra") + wei = prompt_validated( + "Amount to deposit (SUPRA, e.g. 0.5 or 10)", + validate_decimal_amount, "Amount", unit="ether", + ) + account = get_keystore_account() + send_tx(account, cfg("ERC20_SUPRA_HANDLER"), "deposit()", value_wei=wei) + + +def cmd_approve() -> None: + print("Approve ERC20Supra spending for Automation Registry") + wei = prompt_validated( + "Amount to approve (SUPRA, e.g. 0.5 or 10)", + validate_decimal_amount, "Amount", unit="ether", + ) + account = get_keystore_account() + send_tx(account, cfg("ERC20_SUPRA"), "approve(address,uint256)", cfg("DIAMOND"), wei) + + +def _prompt_register_common() -> tuple[str, str, int]: + """Collect payloadTx, predicate, and expiryTime — shared by both register commands.""" + payload_tx = prompt_validated("payloadTx (0x-prefixed hex bytes)", validate_bytes_hex, "payloadTx") + predicate = prompt_validated("predicate (0x-prefixed hex bytes)", validate_bytes_hex, "predicate") + duration = prompt_validated("Duration (seconds, > 0)", validate_int, "Duration", min_value=1) + try: + block_output = run_cast(["block", "latest", "--rpc-url", cfg("RPC_URL")]) + except RuntimeError as e: + raise RuntimeError(f"Could not fetch latest block: {e}") + for line in block_output.splitlines(): + if line.strip().startswith("timestamp"): + parts = line.split() + if len(parts) >= 2: + expiry_time = int(parts[1]) + duration + print(f" Computed expiryTime = {expiry_time}") + return payload_tx, predicate, expiry_time + raise RuntimeError("Could not parse block timestamp from cast output.") + + +def cmd_register() -> None: + print("Register user task") + try: + payload_tx, predicate, expiry_time = _prompt_register_common() + except RuntimeError as e: + print(f" ❌ {e}") + return + max_gas = prompt_validated("maxGasAmount (gas units, e.g. 300000)", validate_int, "maxGasAmount", min_value=1) + gas_price_wei = prompt_validated("Gas price cap (GWEI, e.g. 1 or 0.005)", validate_decimal_amount, "Gas price cap", unit="gwei") + fee_cap_wei = prompt_validated("Automation fee cap for cycle (SUPRA, e.g. 1 or 0.01)", validate_decimal_amount, "Fee cap", unit="ether") + priority = prompt_validated("Priority (integer >= 0)", validate_int, "Priority", min_value=0) + account = get_keystore_account() + send_tx( + account, cfg("DIAMOND"), + "register(bytes,bytes,uint64,uint128,uint128,uint128,uint64,bytes[])", + payload_tx, predicate, str(expiry_time), + str(max_gas), gas_price_wei, fee_cap_wei, str(priority), "[]", + ) + + +def cmd_register_system() -> None: + print("Register system task") + try: + payload_tx, predicate, expiry_time = _prompt_register_common() + except RuntimeError as e: + print(f" ❌ {e}") + return + max_gas = prompt_validated("maxGasAmount (gas units, e.g. 300000)", validate_int, "maxGasAmount", min_value=1) + priority = prompt_validated("Priority (integer >= 0)", validate_int, "Priority", min_value=0) + account = get_keystore_account() + send_tx( + account, cfg("DIAMOND"), + "registerSystemTask(bytes,bytes,uint64,uint128,uint64,bytes[])", + payload_tx, predicate, str(expiry_time), str(max_gas), str(priority), "[]", + ) + + +def cmd_cancel() -> None: _cancel_or_stop("cancelTasks(uint64[])") +def cmd_cancel_system() -> None: _cancel_or_stop("cancelSystemTasks(uint64[])") +def cmd_stop() -> None: _cancel_or_stop("stopTasks(uint64[])") +def cmd_stop_system() -> None: _cancel_or_stop("stopSystemTasks(uint64[])") + + +def cmd_grant_authorization() -> None: + address = prompt_validated("Address to grant authorization to", validate_address) + account = get_keystore_account() + send_tx(account, cfg("DIAMOND"), "grantAuthorization(address)", address) + + +def cmd_revoke_authorization() -> None: + address = prompt_validated("Address to revoke authorization on", validate_address) + account = get_keystore_account() + send_tx(account, cfg("DIAMOND"), "revokeAuthorization(address)", address) + + +# ───────────────────────────────────────────── +# Menu +# ───────────────────────────────────────────── + +COMMANDS: dict[str, tuple[str, Optional[Callable]]] = { + "list-accounts": ("List available keystore accounts", cmd_list_accounts), + "native-balance": ("Show native SUPRA balance", cmd_native_balance), + "erc20Supra-balance": ("Show ERC20Supra balance", cmd_erc20_supra_balance), + "allowance": ("Check ERC20 approval to registry", cmd_allowance), + "deposit": ("Deposit native → mint ERC20Supra", cmd_native_to_erc20), + "approve": ("Approve ERC20Supra for fees", cmd_approve), + "register": ("Register a user task", cmd_register), + "register-system": ("Register a system task", cmd_register_system), + "cancel": ("Cancel user task(s)", cmd_cancel), + "cancel-system": ("Cancel system task(s)", cmd_cancel_system), + "stop": ("Stop user task(s)", cmd_stop), + "stop-system": ("Stop system task(s)", cmd_stop_system), + "grant-authorization": ("Grant authorization to submit GST", cmd_grant_authorization), + "revoke-authorization": ("Revoke authorization to submit GST", cmd_revoke_authorization), + "is-submitter": ("Check if address is authorized submitter", cmd_is_submitter), + "task-details": ("View details of a task", cmd_view_task_details), + "registry-locked-balance": ("View registry locked balance", cmd_registry_locked_balance), + "registry-balance": ("View ERC20Supra balance of registry contract", cmd_registry_balance), + "task-list": ("View all task IDs", cmd_task_list), + "total-tasks": ("View total task count", cmd_total_tasks), + "user-tasks": ("View tasks belonging to a user", cmd_user_tasks), + "task-exists": ("Check whether a task exists", cmd_task_exists), + "exit": ("Quit", None), +} + + +def print_menu() -> None: + col = max(len(k) for k in COMMANDS) + print("\nAutomation Registry CLI\n") + for name, (desc, _) in COMMANDS.items(): + print(f" {name:<{col}} {desc}") + print(f"\n {'help or ?':<{col}} Show this menu again") + + +def main() -> None: + load_environments() + + command_names = list(COMMANDS.keys()) + readline.set_completer(lambda text, state: ( + [c for c in command_names if c.startswith(text)] + [None] + )[state]) + readline.set_completer_delims("") + readline.parse_and_bind("tab: complete") + + print("\n=== Contracts Loaded ===") + print(f" ERC20_SUPRA: {cfg('ERC20_SUPRA')}") + print(f" ERC20_SUPRA_HANDLER: {cfg('ERC20_SUPRA_HANDLER')}") + print(f" DIAMOND: {cfg('DIAMOND')}") + + print_menu() + while True: + try: + cmd = input("\nCommand> ").strip() + except (EOFError, KeyboardInterrupt): + print("\nExiting.") + sys.exit(0) + + print() + + if cmd in ("help", "?"): + print_menu() + continue + + if cmd == "exit": + print("Exiting.") + sys.exit(0) + + if cmd not in COMMANDS: + print(f" Unknown command: '{cmd}' (type 'help' to list commands, Tab to autocomplete)") + continue + + _, fn = COMMANDS[cmd] + try: + fn() + except KeyboardInterrupt: + print("\n (interrupted, returning to menu)") + except Exception as e: + print(f" ❌ Unexpected error: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/solidity/supra_contracts/deploy_automation_registry.sh b/solidity/supra_contracts/deploy_automation_registry.sh new file mode 100755 index 0000000000..3d2d0ed64f --- /dev/null +++ b/solidity/supra_contracts/deploy_automation_registry.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -e + +source .env +: "${RPC_URL:?Missing RPC_URL in .env}" +: "${PRIVATE_KEY:?Missing PRIVATE_KEY in .env}" + +DEPLOY_LOG="deploy.log" +ENV_FILE="deployed.env" + +# Helper for cleaner + safer extraction +extract() { + local result + result=$(grep -m1 "$1" "$DEPLOY_LOG" | grep -o "0x[a-fA-F0-9]\{40\}") + echo "${result:-NOT_FOUND}" +} + +# ------------------------------------------------------------ +# RUN FOUNDRY DEPLOY SCRIPT +# ------------------------------------------------------------ +echo "" +echo "=== Deploying contracts ===" + +ADDRESS=$(cast wallet address --private-key "$PRIVATE_KEY") +export OWNER=$ADDRESS + +forge script script/DeployERC20Supra.s.sol:DeployERC20Supra \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + --skip-simulation \ + -vvvv > "$DEPLOY_LOG" 2>&1 + +ERC20_SUPRA=$(extract "ERC20Supra proxy deployed at: ") +if [[ "$ERC20_SUPRA" == "NOT_FOUND" ]]; then + echo "ERROR: ERC20Supra address not found" + exit 1 +fi + +export ERC20_SUPRA + +forge script script/DeployERC20SupraHandler.s.sol:DeployERC20SupraHandler \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + --skip-simulation \ + -vvvv >> "$DEPLOY_LOG" 2>&1 + +ERC20_SUPRA_HANDLER=$(extract "ERC20SupraHandler proxy deployed at: ") +if [[ "$ERC20_SUPRA_HANDLER" == "NOT_FOUND" ]]; then + echo "ERROR: ERC20SupraHandler address not found" + exit 1 +fi + +export ERC20_SUPRA_HANDLER + +forge script script/DeployDiamond.s.sol:DeployDiamond \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + --skip-simulation \ + -vvvv >> "$DEPLOY_LOG" 2>&1 + +echo "Deployment logs saved to $DEPLOY_LOG" + +# ------------------------------------------------------------ +# PARSE DEPLOYED CONTRACT ADDRESSES +# ------------------------------------------------------------ +echo "" +echo "=== Extracting deployed addresses ===" + +DIAMOND_OWNER=$(extract "Diamond owner:") +DIAMOND=$(extract "Diamond deployed at:") +DIAMOND_CUT_FACET=$(extract "DiamondCutFacet deployed at:") +DIAMOND_LOUPE_FACET=$(extract "DiamondLoupeFacet deployed at:") +OWNERSHIP_FACET=$(extract "OwnershipFacet deployed at:") +CONFIG_FACET=$(extract "ConfigFacet deployed at:") +REGISTRY_FACET=$(extract "RegistryFacet deployed at:") +CORE_FACET=$(extract "CoreFacet deployed at:") +DIAMOND_INIT=$(extract "DiamondInit deployed at:") + +# ------------------------------------------------------------ +# WRITE TO .env +# ------------------------------------------------------------ +echo "" +echo "=== Saving contract addresses to $ENV_FILE ===" +echo "" + +cat < "$ENV_FILE" +# Auto-generated deployment output + +ERC20_SUPRA=$ERC20_SUPRA +ERC20_SUPRA_HANDLER=$ERC20_SUPRA_HANDLER + +DIAMOND_OWNER=$DIAMOND_OWNER +DIAMOND=$DIAMOND +DIAMOND_CUT_FACET=$DIAMOND_CUT_FACET +DIAMOND_LOUPE_FACET=$DIAMOND_LOUPE_FACET +OWNERSHIP_FACET=$OWNERSHIP_FACET +CONFIG_FACET=$CONFIG_FACET +REGISTRY_FACET=$REGISTRY_FACET +CORE_FACET=$CORE_FACET +DIAMOND_INIT=$DIAMOND_INIT +EOF + +cat "$ENV_FILE" + +echo "" +echo "=== Deployment Complete ===" \ No newline at end of file diff --git a/solidity/supra_contracts/foundry.lock b/solidity/supra_contracts/foundry.lock new file mode 100644 index 0000000000..977ce84399 --- /dev/null +++ b/solidity/supra_contracts/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.12.0", + "rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6" + } + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.5.0", + "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" + } + }, + "lib/openzeppelin-contracts-upgradeable": { + "tag": { + "name": "v5.5.0", + "rev": "aa677e9d28ed78fc427ec47ba2baef2030c58e7c" + } + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/foundry.toml b/solidity/supra_contracts/foundry.toml new file mode 100644 index 0000000000..9c89abd938 --- /dev/null +++ b/solidity/supra_contracts/foundry.toml @@ -0,0 +1,31 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +via_ir = true +optimizer = true +evm_version = "prague" +# Fix the solc version to avoid breaking changes in the future. See +solc_version = "0.8.34" +# Do not attach cryptographic hash of compilation to contract's deployed bytecode. +bytecode_hash = "none" +# Prevent the creation and inclusion of this CBOR metadata block. +cbor_metadata = false + +# Raise the block gas limit from Forge's default 2^30 (~1 billion) to accommodate +# MonitorCycleEndGas_BoundaryScan, which performs ~8 binary-search iterations in a +# single test call — each iteration registers up to LARGE_CAPACITY tasks at ~800 k gas +# each, totalling ~1.2 billion gas across the full search. Individual N-point tests +# never exceed ~350 million gas, so this increase has no visible effect on them. +block_gas_limit = 30_000_000_000 +# Uncomment when running agains supra chain + +#eth_rpc_url = "http://localhost:27000/rpc/v1/eth/wallet_integration" + +remappings = [ + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", +] +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + + diff --git a/solidity/supra_contracts/lib/forge-std b/solidity/supra_contracts/lib/forge-std new file mode 160000 index 0000000000..aeb45e9f32 --- /dev/null +++ b/solidity/supra_contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit aeb45e9f32ef8ca78f0aeda17596e9c46374da41 diff --git a/solidity/supra_contracts/lib/openzeppelin-contracts b/solidity/supra_contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000000..8614ef7a24 --- /dev/null +++ b/solidity/supra_contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 8614ef7a24d476e37db66054e5237faaf7f43717 diff --git a/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable b/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000000..a73231f64c --- /dev/null +++ b/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit a73231f64c2a4ab1c0bceb43ba8333be45d2df0a diff --git a/solidity/supra_contracts/script/CheckFacetSelectors.s.sol b/solidity/supra_contracts/script/CheckFacetSelectors.s.sol new file mode 100644 index 0000000000..4b2afa4774 --- /dev/null +++ b/solidity/supra_contracts/script/CheckFacetSelectors.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {DiamondLoupeFacet} from "../src/facets/DiamondLoupeFacet.sol"; +import {OwnershipFacet} from "../src/facets/OwnershipFacet.sol"; +import {ConfigFacet} from "../src/facets/ConfigFacet.sol"; +import {RegistryFacet} from "../src/facets/RegistryFacet.sol"; +import {CoreFacet} from "../src/facets/CoreFacet.sol"; +import {IFacetSelectors} from "../src/interfaces/IFacetSelectors.sol"; +import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol"; + +/// @notice Prints each diamond facet's self-reported `getSelectors()` list, one line per +/// selector, as `SELECTOR `. Deploys throwaway local instances only +/// (no broadcast) — never run with --broadcast, this is a read-only introspection script. +/// +/// Used by check_facet_selectors.sh to cross-check this hand-maintained list against the +/// facet's actual compiled ABI (via `forge inspect methods`) — a `getSelectors()` +/// that omits an entry yields a permanently unroutable function with no compiler error, +/// since Solidity has no way to enforce "this list contains every external function". +/// +/// DiamondCutFacet does not implement IFacetSelectors: Diamond's constructor wires its +/// single selector directly (`IDiamondCut.diamondCut.selector`), not via `getSelectors()`, +/// so it's reported here the same way, for the same cross-check. +contract CheckFacetSelectors is Script { + function run() external { + console.log("SELECTOR", "DiamondCutFacet", vm.toString(abi.encodePacked(IDiamondCut.diamondCut.selector))); + _print("DiamondLoupeFacet", address(new DiamondLoupeFacet())); + _print("OwnershipFacet", address(new OwnershipFacet())); + _print("ConfigFacet", address(new ConfigFacet())); + _print("RegistryFacet", address(new RegistryFacet())); + _print("CoreFacet", address(new CoreFacet())); + } + + function _print(string memory _name, address _facet) internal view { + bytes4[] memory selectors = IFacetSelectors(_facet).getSelectors(); + for (uint256 i; i < selectors.length; i++) { + console.log("SELECTOR", _name, vm.toString(abi.encodePacked(selectors[i]))); + } + } +} diff --git a/solidity/supra_contracts/script/DeployBlockMeta.s.sol b/solidity/supra_contracts/script/DeployBlockMeta.s.sol new file mode 100644 index 0000000000..3af43862db --- /dev/null +++ b/solidity/supra_contracts/script/DeployBlockMeta.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {BlockMeta} from "../src/BlockMeta.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployBlockMeta is Script { + address automationController; + bytes4 selector; + address owner; + + function setUp() public { + automationController = vm.envAddress("AUTOMATION_CONTROLLER"); + selector = bytes4(keccak256("monitorCycleEnd()")); + owner = vm.envAddress("OWNER"); + } + + function run() public { + vm.startBroadcast(); + + // Deploy BlockMeta implementation + BlockMeta impl = new BlockMeta(); + console.log("BlockMeta implementation deployed at: ", address(impl)); + + + // Deploy BlockMeta proxy + bytes memory initData = abi.encodeCall(BlockMeta.initialize, (owner, 1_000_000)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + console.log("BlockMeta proxy deployed at: ", address(proxy)); + + // Register the selector + BlockMeta(address(proxy)).register(automationController, selector, 100_000); + + vm.stopBroadcast(); + } +} diff --git a/solidity/supra_contracts/script/DeployDiamond.s.sol b/solidity/supra_contracts/script/DeployDiamond.s.sol new file mode 100644 index 0000000000..d490d4ca7d --- /dev/null +++ b/solidity/supra_contracts/script/DeployDiamond.s.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {OwnershipFacet} from "../src/facets/OwnershipFacet.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; + +contract DeployDiamond is Script { + address erc20Supra; + address multiSig; + + InitParams initParams; + + // Config values loaded from .env file + function setUp() public { + initParams = InitParams({ + taskDurationCapSecs: uint64(vm.envUint("TASK_DURATION_CAP_SEC")), + registryMaxGasCap: uint128(vm.envUint("REGISTRY_MAX_GAS_CAP")), + automationBaseFeeWeiPerSec: uint128(vm.envUint("AUTOMATION_BASE_FEE_PER_SEC")), + flatRegistrationFeeWei: uint128(vm.envUint("FLAT_REGISTRATION_FEE")), + congestionThresholdPercentage: uint8(vm.envUint("CONGESTION_THRESHOLD_PERCENTAGE")), + congestionBaseFeeWeiPerSec: uint128(vm.envUint("CONGESTION_BASE_FEE_PER_SEC")), + congestionExponent: uint8(vm.envUint("CONGESTION_EXPONENT")), + maxCongestionExponent: uint8(vm.envUint("MAX_CONGESTION_EXPONENT")), + taskCapacity: uint16(vm.envUint("TASK_CAPACITY")), + cycleDurationSecs: uint64(vm.envUint("CYCLE_DURATION_SEC")), + sysTaskDurationCapSecs: uint64(vm.envUint("SYS_TASK_DURATION_CAP_SEC")), + sysRegistryMaxGasCap: uint128(vm.envUint("SYS_REGISTRY_MAX_GAS_CAP")), + sysTaskCapacity: uint16(vm.envUint("SYS_TASK_CAPACITY")), + automationEnabled: vm.envBool("AUTOMATION_ENABLED"), + registrationEnabled: vm.envBool("REGISTRATION_ENABLED") + }); + + erc20Supra = vm.envAddress("ERC20_SUPRA"); + multiSig = vm.envAddress("MULTI_SIG"); + } + + function run() external { + vm.startBroadcast(); + + // Deploy the Diamond, its facets and the DiamondInit and initialize the Diamond in a single transaction + Deployment memory deployment = LibDiamondUtils.deploy(multiSig, erc20Supra, initParams); + + console.log("Diamond owner:", OwnershipFacet(address(deployment.diamond)).owner()); + console.log("Diamond deployed at:", address(deployment.diamond)); + console.log("DiamondCutFacet deployed at:", address(deployment.facets.diamondCutFacet)); + console.log("DiamondLoupeFacet deployed at:", address(deployment.facets.loupeFacet)); + console.log("OwnershipFacet deployed at:", address(deployment.facets.ownershipFacet)); + console.log("ConfigFacet deployed at:", address(deployment.facets.configFacet)); + console.log("RegistryFacet deployed at:", address(deployment.facets.registryFacet)); + console.log("CoreFacet deployed at:", address(deployment.facets.coreFacet)); + console.log("DiamondInit deployed at:", address(deployment.facets.diamondInit)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/script/DeployERC20Supra.s.sol b/solidity/supra_contracts/script/DeployERC20Supra.s.sol new file mode 100644 index 0000000000..94316e5031 --- /dev/null +++ b/solidity/supra_contracts/script/DeployERC20Supra.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployERC20Supra is Script { + address owner; + address[] authorizedAddresses; + + function setUp() public { + owner = vm.envAddress("OWNER"); + address bridge = vm.envAddress("BRIDGE"); + address erc20SupraHandler = vm.envAddress("ERC20SUPRA_HANDLER"); + + // Create array with authorized addresses + authorizedAddresses = new address[](2); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = erc20SupraHandler; + } + + function run() public { + vm.startBroadcast(); + + // Deploy ERC20Supra implementation + ERC20Supra impl = new ERC20Supra(); + console.log("ERC20Supra implementation deployed at: ", address(impl)); + + // Deploy ERC20Supra proxy + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (owner, authorizedAddresses)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + console.log("ERC20Supra proxy deployed at: ", address(proxy)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/script/DeployERC20SupraHandler.s.sol b/solidity/supra_contracts/script/DeployERC20SupraHandler.s.sol new file mode 100644 index 0000000000..7150d92652 --- /dev/null +++ b/solidity/supra_contracts/script/DeployERC20SupraHandler.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployERC20SupraHandler is Script { + address owner; + address erc20Supra; + + function setUp() public { + owner = vm.envAddress("OWNER"); + erc20Supra = vm.envAddress("ERC20_SUPRA"); + } + + function run() public { + vm.startBroadcast(); + + // Deploy ERC20SupraHandler implementation + ERC20SupraHandler impl = new ERC20SupraHandler(); + console.log("ERC20SupraHandler implementation deployed at: ", address(impl)); + + // Deploy ERC20SupraHandler proxy + bytes memory initData = abi.encodeCall(ERC20SupraHandler.initialize, (owner, erc20Supra)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + console.log("ERC20SupraHandler proxy deployed at: ", address(proxy)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/script/DeployMultisig.s.sol b/solidity/supra_contracts/script/DeployMultisig.s.sol new file mode 100644 index 0000000000..319edd1502 --- /dev/null +++ b/solidity/supra_contracts/script/DeployMultisig.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {MultiSignatureWallet} from "../src/MultiSignatureWallet.sol"; +import {MultisigBeacon} from "../src/MultisigBeacon.sol"; +import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +contract DeployMultisig is Script { + address[] owners; + uint256 numConfirmations; + address beaconOwner; + + function setUp() public { + owners = vm.envAddress("OWNERS", ","); + numConfirmations = vm.envUint("NUM_CONFIRMATIONS"); + beaconOwner = vm.envAddress("BEACON_OWNER"); + } + + function run() public { + vm.startBroadcast(); + + // --------------------------------- + // Deploy multisig implementation + // --------------------------------- + MultiSignatureWallet multisigImpl = new MultiSignatureWallet(); + console.log("Multisig implementation deployed at: ", address(multisigImpl)); + + // ------------------------------------------- + // Deploy beacon pointing to implementation + // ------------------------------------------- + MultisigBeacon beacon = new MultisigBeacon(address(multisigImpl), beaconOwner); + console.log("Beacon deployed at: ", address(beacon)); + console.log("Beacon owner: ", beacon.owner()); + + // ---------------------- + // Deploy multisig proxy + // ---------------------- + console.log("Number of confirmations: ", numConfirmations); + console.log("Adding following owners: "); + for (uint i = 0; i < owners.length; i++) { + console.logAddress(owners[i]); + } + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (owners, numConfirmations)); + BeaconProxy multisigProxy = new BeaconProxy(address(beacon), initData); + console.log("Multisig Proxy deployed at: ", address(multisigProxy)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/script/GovActions.s.sol b/solidity/supra_contracts/script/GovActions.s.sol new file mode 100644 index 0000000000..5586b63910 --- /dev/null +++ b/solidity/supra_contracts/script/GovActions.s.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {MultiSignatureWallet} from "../src/MultiSignatureWallet.sol"; +import {BlockMeta} from "../src/BlockMeta.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; + +contract InitializeCycleMonitoring is Script { + address payable multisigWalletAddr; + address blockMetadata; + address registry; + bytes4 selector; + uint64 selectorGasLimit; + uint64 timeout; + + function setUp() public { + multisigWalletAddr = payable(vm.envAddress("MULTISIG_WALLET_ADDRESS")); + blockMetadata = vm.envAddress("BLOCK_METADATA_ADDRESS"); + registry = vm.envAddress("REGISTRY"); + selector = bytes4(keccak256("monitorCycleEnd()")); + // if gas-selectorGasLimit is greater than the block prologue gas cap, + // the transaction will fail and the cycle monitoring will not be registered + selectorGasLimit = uint64(vm.envUint("SELECTOR_GAS_LIMIT")); + timeout = uint64(vm.envUint("TIMEOUT")); + } + + function run() public { + vm.startBroadcast(); + + // Initialize MultiSignatureWallet and get nextTxnIndex + MultiSignatureWallet wallet = MultiSignatureWallet(multisigWalletAddr); + uint256 nextTxnIndex = wallet.getNextTransactionIndex(); + console.log("TxnIndex: ", nextTxnIndex); + + // Submit a foundation/gov action to register registry::monitor_cycle_event + // to be executed for each block + bytes memory data = abi.encodeCall(BlockMeta.register, (registry, selector, selectorGasLimit)); + wallet.submitTransaction(blockMetadata, 0, timeout, data); + + vm.stopBroadcast(); + } +} + +contract AuthorizeAccount is Script { + address payable multisigWalletAddr; + address automationRegistry; + address account; + uint64 timeout; + + function setUp() public { + multisigWalletAddr = payable(vm.envAddress("MULTISIG_WALLET_ADDRESS")); + automationRegistry = vm.envAddress("REGISTRY"); + account = vm.envAddress("ACCOUNT_TO_AUTHORIZE"); + timeout = uint64(vm.envUint("TIMEOUT")); + } + + function run() public { + vm.startBroadcast(); + + // Initialize MultiSignatureWallet and get nextTxnIndex + MultiSignatureWallet wallet = MultiSignatureWallet(multisigWalletAddr); + uint256 nextTxnIndex = wallet.getNextTransactionIndex(); + console.log("TxnIndex: ", nextTxnIndex); + + // Submit a foundation/gov action to grant authorization for gst task registration + bytes memory data = abi.encodeCall(IConfigFacet.grantAuthorization, (account)); + wallet.submitTransaction(automationRegistry, 0, timeout, data); + + vm.stopBroadcast(); + } +} + +contract EnableDisableAutomation is Script { + address payable multisigWalletAddr; + address automationRegistry; + address account; + uint64 timeout; + bool enable; + + function setUp() public { + multisigWalletAddr = payable(vm.envAddress("MULTISIG_WALLET_ADDRESS")); + automationRegistry = vm.envAddress("REGISTRY"); + account = vm.envAddress("ACCOUNT_TO_AUTHORIZE"); + timeout = uint64(vm.envUint("TIMEOUT")); + enable = bool(vm.envBool("ENABLE_AUTOMATION")); + } + + function run() public { + vm.startBroadcast(); + + // Initialize MultiSignatureWallet and get nextTxnIndex + MultiSignatureWallet wallet = MultiSignatureWallet(multisigWalletAddr); + uint256 nextTxnIndex = wallet.getNextTransactionIndex(); + console.log("TxnIndex: ", nextTxnIndex); + console.log("Automation Flag: ", ICoreFacet(automationRegistry).isAutomationEnabled()); + + // Submit a foundation/gov action to enable/disable automation + bytes memory data = hex""; + if (enable) { + data = abi.encodeCall(ICoreFacet.enableAutomation, ()); + } else { + data = abi.encodeCall(ICoreFacet.disableAutomation, ()); + } + wallet.submitTransaction(automationRegistry, 0, timeout, data); + + vm.stopBroadcast(); + } +} + +contract VoteForTxn is Script { + address payable multisigWalletAddr; + uint256 txIndex; + + + function setUp() public { + multisigWalletAddr = payable(vm.envAddress("MULTISIG_WALLET_ADDRESS")); + txIndex = uint256(vm.envUint("GOV_TXN_INDEX")); + } + + function run() public { + vm.startBroadcast(); + MultiSignatureWallet wallet = MultiSignatureWallet(multisigWalletAddr); + console.log("Txn count", wallet.txCount()); + wallet.confirmTransaction(txIndex); + vm.stopBroadcast(); + } +} + +contract ExecuteTxn is Script { + address payable multisigWalletAddr; + uint256 txIndex; + + + function setUp() public { + multisigWalletAddr = payable(vm.envAddress("MULTISIG_WALLET_ADDRESS")); + txIndex = uint256(vm.envUint("GOV_TXN_INDEX")); + } + + function run() public { + vm.startBroadcast(); + MultiSignatureWallet wallet = MultiSignatureWallet(multisigWalletAddr); + wallet.executeTransaction(txIndex); + vm.stopBroadcast(); + } +} diff --git a/solidity/supra_contracts/script/MintErc20Supra.s.sol b/solidity/supra_contracts/script/MintErc20Supra.s.sol new file mode 100644 index 0000000000..11e7129bb0 --- /dev/null +++ b/solidity/supra_contracts/script/MintErc20Supra.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; + +contract MintErc20Supra is Script { + uint64 value; + uint64 allowance; + address erc20SupraAddr; + address payable erc20SupraHandlerAddr; + address authority; + + // Config values loaded from .env file + function setUp() public { + value = uint64(vm.envUint("VALUE")); + allowance = uint64(vm.envUint("ALLOWANCE")); + erc20SupraAddr = vm.envAddress("ERC20SUPRA"); + erc20SupraHandlerAddr = payable(vm.envAddress("ERC20SUPRA_HANDLER")); + authority = vm.envAddress("REGISTRY"); + } + + function run() public { + vm.startBroadcast(); + + ERC20Supra erc20Supra = ERC20Supra(erc20SupraAddr); + ERC20SupraHandler erc20SupraHandler = ERC20SupraHandler(erc20SupraHandlerAddr); + console.log("Sender: ", msg.sender); + console.log("Token balance before: ", erc20Supra.balanceOf(msg.sender)); + + // First approve the authority to spend tokens + erc20Supra.approve(authority, uint256(allowance)); + console.log("Approved authority for allowance: ", allowance); + + // Then do the conversion + erc20SupraHandler.deposit{value: value}(); + uint256 confAll = erc20Supra.allowance(msg.sender, authority); + + console.log("Sender: ", msg.sender, confAll, authority); + console.log("Token balance after: ", erc20Supra.balanceOf(msg.sender)); + + vm.stopBroadcast(); + } + +} diff --git a/solidity/supra_contracts/script/RegisterAutomationTask.s.sol b/solidity/supra_contracts/script/RegisterAutomationTask.s.sol new file mode 100644 index 0000000000..7209dc8c5b --- /dev/null +++ b/solidity/supra_contracts/script/RegisterAutomationTask.s.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Script, console} from "forge-std/Script.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {TxHashPrecompile} from "./TxHashPrecompile.sol"; + +contract RegisterAutomationTask is Script { + uint64 taskDurationSecs; + uint64 automationFeeCap; + uint128 taskMaxGas; + uint128 taskGasPriceCap; + address registry; + address erc20supra; + address target; + uint128 amountToTransfer; + + address public constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + // Config values loaded from .env file + function setUp() public { + taskDurationSecs = uint64(vm.envUint("TASK_DURATION_SEC")); + taskMaxGas = uint128(vm.envUint("TASK_MAX_GAS")); + taskGasPriceCap = uint128(vm.envUint("TASK_GAS_PRICE_CAP")); + registry = vm.envAddress("REGISTRY"); + erc20supra = vm.envAddress("ERC20SUPRA"); + target = vm.envAddress("TARGET"); + amountToTransfer = uint128(vm.envUint("AMOUNT_TO_TRANSFER")); + automationFeeCap = uint64(vm.envUint("TASK_AUTOMATION_FEE_CAP")); + + // Deploy TxHashPrecompile and etch its runtime code at the precompile address + // Helps with precompilation but not with simulation, so one need to run the script with --skip-simualation flag + TxHashPrecompile deployed = new TxHashPrecompile(); + vm.etch(TX_HASH_PRECOMPILE, address(deployed).code); + } + + function run() public { + vm.startBroadcast(); + IRegistryFacet registryFacet = IRegistryFacet(registry); + bytes[] memory auxData; + uint64 taskIdx = registryFacet.getNextTaskIndex(); + console.log("Next task index ", taskIdx); + + bytes memory payload = createPayload(0, amountToTransfer, target, erc20supra); + bytes memory predicate = createPredicate(registry); + + registryFacet.register( + payload, + predicate, + uint64(block.timestamp + taskDurationSecs), // Task expires before next cycle + taskMaxGas, + taskGasPriceCap, + automationFeeCap, + 0, + auxData + ); + + vm.stopBroadcast(); + } + + function createPayload(uint128 _value, uint128 _amountToTransfer, address recipient, address cAddress) private pure returns (bytes memory) { + LibCommon.AccessListEntry[] memory accessList = new LibCommon.AccessListEntry[](0); + bytes memory callData = abi.encodeCall(IERC20.transfer, (recipient, _amountToTransfer)); + bytes memory payload = abi.encode(_value, cAddress, callData, accessList); + + return payload; + } + + function createPredicate(address _target) private pure returns (bytes memory) { + // Create a predicate that checks if registration is enabled + bytes memory callData = abi.encodeCall(IConfigFacet.isRegistrationEnabled, ()); + return abi.encode(_target, callData); + } +} + +contract RegisterGaslessAutomationTask is Script { + uint64 taskDurationSecs; + uint128 taskMaxGas; + address registry; + address erc20supra; + address target; + uint128 amountToTransfer; + + address public constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + // Config values loaded from .env file + function setUp() public { + taskDurationSecs = uint64(vm.envUint("TASK_DURATION_SEC")); + taskMaxGas = uint128(vm.envUint("TASK_MAX_GAS")); + registry = vm.envAddress("REGISTRY"); + erc20supra = vm.envAddress("ERC20SUPRA"); + target = vm.envAddress("TARGET"); + amountToTransfer = uint128(vm.envUint("AMOUNT_TO_TRANSFER")); + + // Deploy TxHashPrecompile and etch its runtime code at the precompile address + // Helps with precompilation but not with simulation, so one need to run the script with --skip-simualation flag + TxHashPrecompile deployed = new TxHashPrecompile(); + vm.etch(TX_HASH_PRECOMPILE, address(deployed).code); + } + + function run() public { + vm.startBroadcast(); + IRegistryFacet registryFacet = IRegistryFacet(registry); + bytes[] memory auxData; + uint64 taskIdx = registryFacet.getNextTaskIndex(); + console.log("Next task index ", taskIdx); + + bytes memory payload = createPayload(0, amountToTransfer, target, erc20supra); + bytes memory predicate = createPredicate(registry); + + registryFacet.registerSystemTask( + payload, + predicate, + uint64(block.timestamp + taskDurationSecs), // Task expires before next cycle + taskMaxGas, + 0, + auxData + ); + + vm.stopBroadcast(); + } + + function createPayload(uint128 _value, uint128 _amountToTransfer, address recipient, address cAddress) private pure returns (bytes memory) { + LibCommon.AccessListEntry[] memory accessList = new LibCommon.AccessListEntry[](0); + bytes memory callData = abi.encodeCall(IERC20.transfer, (recipient, _amountToTransfer)); + bytes memory payload = abi.encode(_value, cAddress, callData, accessList); + + return payload; + } + + function createPredicate(address _target) private pure returns (bytes memory) { + // Create a predicate that checks if registration is enabled + bytes memory callData = abi.encodeCall(IConfigFacet.isRegistrationEnabled, ()); + return abi.encode(_target, callData); + } +} + +contract CancelAutomationTask is Script { + address registry; + uint64 taskIndex; + + address public constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + // Config values loaded from .env file + function setUp() public { + registry = vm.envAddress("REGISTRY"); + taskIndex = uint64(vm.envUint("TASK_INDEX")); + + TxHashPrecompile deployed = new TxHashPrecompile(); + vm.etch(TX_HASH_PRECOMPILE, address(deployed).code); + } + + function run() public { + vm.startBroadcast(); + IRegistryFacet registryFacet = IRegistryFacet(registry); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = taskIndex; + registryFacet.cancelTasks(taskIndexes); + + vm.stopBroadcast(); + } + +} diff --git a/solidity/supra_contracts/script/TxHashPrecompile.sol b/solidity/supra_contracts/script/TxHashPrecompile.sol new file mode 100644 index 0000000000..1cc7779695 --- /dev/null +++ b/solidity/supra_contracts/script/TxHashPrecompile.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + + +contract TxHashPrecompile { + fallback(bytes calldata input) external returns (bytes memory) { + bytes32 output = keccak256("txn_hash"); + return abi.encode( output); + } +} diff --git a/solidity/supra_contracts/script/check_facet_selectors.sh b/solidity/supra_contracts/script/check_facet_selectors.sh new file mode 100755 index 0000000000..b173d1d357 --- /dev/null +++ b/solidity/supra_contracts/script/check_facet_selectors.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Cross-checks each diamond facet's hand-maintained `getSelectors()` list against the +# facet's actual compiled ABI (`forge inspect methods`). +# +# `getSelectors()` is trusted verbatim by Diamond's constructor when wiring the diamond's +# routing table (see Diamond.sol). Solidity has no way to enforce "this list contains every +# external function this contract defines" — an entry can be silently omitted by mistake, +# which yields a permanently unroutable function with no compiler error. This script makes +# that mechanical: any external/public function that solc's own ABI reports but that does +# not appear in the facet's `getSelectors()` output is flagged as a FAIL. +# +# There is no CI wiring for the Solidity contracts in this repo yet (checked +# .github/workflows/*.yml — none reference forge/foundry/supra_contracts), so this is a +# standalone script for now, run manually or wired into CI as a follow-up. +# +# Usage: ./script/check_facet_selectors.sh (run from supra_contracts/, or anywhere — it cd's there) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$SCRIPT_DIR" + + +echo "Running CheckFacetSelectors.s.sol to collect each facet's self-reported getSelectors()..." +SCRIPT_OUTPUT="$(forge script script/CheckFacetSelectors.s.sol 2>&1 || true)" + +if ! grep -q "^ SELECTOR " <<<"$SCRIPT_OUTPUT"; then + echo "FAIL: CheckFacetSelectors.s.sol produced no SELECTOR lines. Full output:" + echo "$SCRIPT_OUTPUT" + exit 1 +fi + +overall_status=0 + +# Ground-truth expected facet set: every contract under src/facets/ (recursively) whose +# compiled ABI includes getSelectors() (i.e. implements IFacetSelectors) -- derived from +# forge's own compiled-artifact introspection, not from this script's source text, not from +# CheckFacetSelectors.s.sol's deploy list, and not from this run's SELECTOR output. That +# independence closes two different holes at once: a facet whose getSelectors() reports +# zero entries no longer vanishes from both sides of the comparison silently, AND a facet +# that implements IFacetSelectors but was never added to CheckFacetSelectors.s.sol's run() +# in the first place is caught too. Using the compiled ABI rather than grepping the `is` +# clause also means this is immune to how that clause happens to be formatted (a multi-line +# inheritance list is not a special case) and can't be fooled by a commented-out declaration. +# DiamondCutFacet is added as one documented exception: it deliberately does not implement +# IFacetSelectors (Diamond's constructor wires its one selector directly), so its ABI never +# has getSelectors() and it can't be found this way -- see CheckFacetSelectors.s.sol's own +# note on that. +expected_facets=() +while IFS= read -r -d '' sol_file; do + contract_name="$(basename "$sol_file" .sol)" + if forge inspect "$contract_name" methods --json 2>/dev/null | jq -e 'has("getSelectors()")' >/dev/null 2>&1; then + expected_facets+=("$contract_name") + fi +done < <(find src/facets -name '*.sol' -print0) +expected_facets+=("DiamondCutFacet") +expected_facets=( $(printf '%s\n' "${expected_facets[@]}" | sort -u) ) + +reported_facets=( $( + grep "^ SELECTOR" <<<"$SCRIPT_OUTPUT" \ + | awk '{print $2}' \ + | sort -u + ) ) + +# Check membership in both directions: a facet that implements IFacetSelectors but never +# shows up in the run's output (missing from CheckFacetSelectors.s.sol's run(), or its +# getSelectors() reverts/reports zero entries) is a FAIL, not a silent skip; a facet that +# shows up in the output but doesn't implement IFacetSelectors in its own source (a typo'd +# name, or a stale entry for a removed facet) is flagged too, since the two have drifted apart. +for expected in "${expected_facets[@]}"; do + if ! printf '%s\n' "${reported_facets[@]}" | grep -qx "$expected"; then + overall_status=1 + echo "FAIL: $expected — implements IFacetSelectors but reported zero selectors (missing from CheckFacetSelectors.s.sol's run(), or getSelectors() is broken)" + fi +done +for reported in "${reported_facets[@]}"; do + if ! printf '%s\n' "${expected_facets[@]}" | grep -qx "$reported"; then + overall_status=1 + echo "FAIL: $reported — reported selectors but does not implement IFacetSelectors in src/facets/ (stale or typo'd entry in CheckFacetSelectors.s.sol)" + fi +done + +for facet in "${reported_facets[@]}"; do + # Ground truth: every external/public function's selector, per solc's own ABI. + # getSelectors() itself is excluded — it is deliberately not meant to be routed. + inspected_selectors="$( + forge inspect "$facet" methods --json \ + | jq -r 'to_entries[] | select(.key != "getSelectors()") | .value' \ + | tr '[:upper:]' '[:lower:]' \ + | sort -u + )" + + # What the facet's own getSelectors() (or, for DiamondCutFacet, Diamond's constructor's + # hardcoded IDiamondCut.diamondCut.selector) actually reports. The `|| true` on the grep + # itself (not the whole pipeline) matters: under `pipefail`, grep matching nothing would + # otherwise abort the script before any FAIL is ever reported -- exactly the "this facet + # reported zero selectors" case this checker exists to catch. Isolating it here keeps + # reported_selectors a clean (possibly empty) list of hex selectors, so a genuine zero- + # selector facet correctly shows every one of its real selectors as "missing" below, + # instead of polluting the comparison with a human-readable fallback string. + reported_selectors="$( + { grep "^ SELECTOR $facet " <<<"$SCRIPT_OUTPUT" || true; } \ + | awk '{print $3}' \ + | sed 's/^0x//' \ + | tr '[:upper:]' '[:lower:]' \ + | sort -u + )" + + missing="$(comm -23 <(echo "$inspected_selectors") <(echo "$reported_selectors") | sed '/^$/d')" + extra="$(comm -13 <(echo "$inspected_selectors") <(echo "$reported_selectors") | sed '/^$/d')" + + if [[ -z "$missing" && -z "$extra" ]]; then + echo "PASS: $facet — getSelectors() matches the compiled ABI exactly." + else + overall_status=1 + echo "FAIL: $facet" + if [[ -n "$missing" ]]; then + echo " Selectors solc reports that getSelectors() is missing (permanently unroutable if added to the diamond):" + while read -r sel; do + name="$(jq -r --arg s "$sel" 'to_entries[] | select((.value | ascii_downcase) == $s) | .key' <(forge inspect "$facet" methods --json))" + echo " 0x$sel $name" + done <<<"$missing" + fi + if [[ -n "$extra" ]]; then + echo " Selectors getSelectors() reports that solc's ABI does not have (should not happen — investigate):" + while read -r sel; do + echo " 0x$sel" + done <<<"$extra" + fi + fi +done + +exit $overall_status diff --git a/solidity/supra_contracts/src/BlockMeta.sol b/solidity/supra_contracts/src/BlockMeta.sol new file mode 100644 index 0000000000..da7f4a6cc9 --- /dev/null +++ b/solidity/supra_contracts/src/BlockMeta.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {LibUtils} from "./libraries/LibUtils.sol"; +import {IBlockMeta} from "./interfaces/IBlockMeta.sol"; + +/** + * BlockMeta is a system-level execution scheduler — it maintains an ordered queue of (target contract, selector) pairs + * that the Supra VM fires once per block at block-start time via blockPrologue(). + * Think of it as a deterministic cron-within-a-block for system-level hooks (oracle updates, reward distributions, etc.) + * that must run on every block without user transactions. + */ + +contract BlockMeta is OwnableUpgradeable, UUPSUpgradeable, IBlockMeta { + using LibUtils for address; + + /// @dev Mask to extract the (target | selector) key, zeroing out the gas limit bits. + uint256 private constant KEY_MASK = type(uint256).max << 64; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * STORAGE + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + /// @notice Total gas cap for the entire blockPrologue execution. + /// @dev Checked at registration time; sum of all per-entry gas limits must not exceed this. + uint64 public blockPrologueGasCap; + + /// @notice Sum of all per-entry gas limits. + uint64 public totalGasAllocated; + + + /// @notice Ordered list of functions to be executed + /// @dev Layout: [target[160] | selector[32] | gasLimit[64]] + uint256[] private executions; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * CONSTRUCTOR AND INITIALIZER + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the owner and sets the block prologue gas cap. + /// @param _initialOwner Address of the contract owner. + /// @param _gasCap Total gas cap for blockPrologue execution (sum of per-entry gas limits must not exceed this). + function initialize(address _initialOwner, uint64 _gasCap) public initializer { + __Ownable_init(_initialOwner); + require(_gasCap > 0, InvalidGasCap()); + blockPrologueGasCap = _gasCap; + } + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * ADMIN FUNCTIONS + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + /// @notice Registers a function selector. + /// @param _targetContract The target contract address. + /// @param _selector Function selector to be called on target contract. + /// @param _gasLimit Gas limit for this function. + function register(address _targetContract, bytes4 _selector, uint64 _gasLimit) external onlyOwner { + _targetContract.validateContractAddress(); + + require(_selector != bytes4(0), InvalidSelector()); + require(_gasLimit > 0, InvalidGasLimit()); + // Widen to uint256 so a near-uint64-max totalGasAllocated can't overflow the addition + // and mask the intended GasCapExceeded() error behind a raw arithmetic Panic(0x11). + // Make sure that 63/64 forwarding rule will be actual for totalGasAllocated for the registered entries. + uint256 upperBound = forwardingRuleCompatibleUpperBoundGasCap(blockPrologueGasCap); + require(uint256(totalGasAllocated) + _gasLimit <= upperBound, GasCapExceeded()); + + uint256 executionEntry = packExecution(_targetContract, _selector); + + // Check to prevent duplicate entries, reverts if already registered + checkDuplicate(executionEntry); + + // Add to the execution order with gas limit packed in + executions.push(executionEntry | _gasLimit); + totalGasAllocated += _gasLimit; + + emit SelectorRegistered(_targetContract, _selector, _gasLimit); + } + + /// @notice Deregisters a function selector. + /// @param _targetContract The target contract address. + /// @param _selector The function selector to deregister. + function deregister(address _targetContract, bytes4 _selector) external onlyOwner { + uint256 executionEntry = packExecution(_targetContract, _selector); + + uint256 index = findIndex(executionEntry); + removeAt(index); + } + + /// @notice Deregisters a function selector. + /// @param _index Index in the `executions` array. + function deregisterAt(uint256 _index) external onlyOwner { + require(_index < executions.length, InvalidIndex()); + removeAt(_index); + } + + /// @notice Updates the entire execution order. + /// @dev _executions entries must be packed as [target(160) | selector(32) | gasLimit(64)] + /// @param _executions An array of packed execution entries representing the new execution order. + function updateExecutionOrder(uint256[] calldata _executions) external onlyOwner { + uint256 inputCount = _executions.length; + require(inputCount > 0, InvalidExecutionsLength()); + + // Clear existing array + delete executions; + uint256 upperBound = forwardingRuleCompatibleUpperBoundGasCap(blockPrologueGasCap); + + // Accumulate in uint256 so a run of near-uint64-max gas limits can't overflow the + // running total and mask the intended GasCapExceeded() error behind Panic(0x11). + uint256 newTotalGas = 0; + + for (uint256 i = 0; i < inputCount; i++) { + uint256 inputExecution = _executions[i]; + (address target, bytes4 selector) = unpackExecution(inputExecution); + uint64 gasLimit = uint64(inputExecution); + + // Input validation + target.validateContractAddress(); + require(selector != bytes4(0), InvalidSelector()); + require(gasLimit > 0, InvalidGasLimit()); + + // Check to prevent duplicate entries, reverts if already registered + checkDuplicate(inputExecution); + + executions.push(inputExecution); + newTotalGas += gasLimit; + require(newTotalGas <= upperBound, GasCapExceeded()); + } + + // Safe to downcast: the loop's require guarantees newTotalGas <= blockPrologueGasCap, + // which is itself a uint64, so it always fits. + totalGasAllocated = uint64(newTotalGas); + + emit ExecutionOrderUpdated(_executions); + } + + /// @notice Sets the total gas cap for the block prologue. + /// @param _cap The new total gas cap (where _cap * 63/64 >= current total allocated gas). + /// @dev This function should be used with caution, the increase should be cross-checked with downstream block-metadata + /// transaction gas-limit value and the new value should never exceed it. + function setBlockPrologueGasCap(uint64 _cap) external onlyOwner { + uint256 upperBound = forwardingRuleCompatibleUpperBoundGasCap(_cap); + require(_cap > 0 && upperBound >= totalGasAllocated, InvalidGasCap()); + blockPrologueGasCap = _cap; + emit BlockPrologueGasCapUpdated(_cap); + } + + /// @notice Calls all registered functions for the targets. + function blockPrologue() external { + msg.sender.enforceIsVmSigner(); // Caller must be VM Signer + + uint256 len = executions.length; + for (uint256 i = 0; i < len; i++) { + uint256 entry = executions[i]; + uint64 gasLimit = uint64(entry); + (address target, bytes4 selector) = unpackExecution(entry); + (bool ok, bytes memory data) = target.call{gas: gasLimit}(abi.encodePacked(selector)); + if (ok) { + emit CallSucceeded(target, selector); + } else { + emit CallFailed(target, selector, data); + } + } + } + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * HELPER FUNCTIONS + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + /// @notice Packs a target contract address and function selector into a single uint256 execution entry. + /// @param _targetContract The target contract address. + /// @param _selector The function selector on the target contract. + /// @return executionEntry The uint256 representing the packed execution entry. + function packExecution(address _targetContract, bytes4 _selector) private pure returns (uint256) { + // Layout: [target[160] | selector[32] | 0[64] ] + return (uint256(uint160(_targetContract)) << 96) | (uint256(uint32(_selector)) << 64); + } + + /// @notice Unpacks an execution entry into its target contract and function selector. + /// @param _executionEntry The packed execution entry to unpack. + /// @return target The target contract address. + /// @return selector The function selector on the target contract. + function unpackExecution(uint256 _executionEntry) private pure returns (address target, bytes4 selector) { + target = address(uint160(_executionEntry >> 96)); + selector = bytes4(uint32(_executionEntry >> 64)); + } + + /// @notice Checks whether a given (target, selector) pair is already registered. + /// @dev Compares only the target+selector bits, ignoring the gas limit. + /// @param _executionEntry The packed execution entry to check (gas bits are masked out). + function checkDuplicate(uint256 _executionEntry) private view { + uint256 len = executions.length; + for (uint256 i = 0; i < len; i++) { + if ((executions[i] & KEY_MASK) == (_executionEntry & KEY_MASK)) { + revert SelectorAlreadyRegistered(); + } + } + } + + /// @notice Finds the index of a given (target, selector) pair in the `executions` array. + /// @dev Compares only the target+selector bits, ignoring the gas limit. + /// @param _executionEntry The packed execution entry to search for (gas bits are masked out). + /// @return index The index of the execution entry in the `executions` array. + function findIndex(uint256 _executionEntry) private view returns (uint256) { + uint256 len = executions.length; + for (uint256 i = 0; i < len; i++) { + if ((executions[i] & KEY_MASK) == (_executionEntry & KEY_MASK)) { + return i; + } + } + revert SelectorNotRegistered(); + } + + /// @notice Helper function to remove an entry from the `executions` array. + /// @param _index Index of the execution entry to be removed. + function removeAt(uint256 _index) private { + uint256 len = executions.length; + uint256 removedEntry = executions[_index]; + uint64 gasLimit = uint64(removedEntry); + totalGasAllocated -= gasLimit; + + for (uint256 i = _index; i < len - 1; i++) { + executions[i] = executions[i + 1]; + } + + executions.pop(); + + (address target, bytes4 selector) = unpackExecution(removedEntry); + + emit SelectorDeregistered(target, selector, gasLimit); + } + + // Helps to calculate a loose upper bound (not an absolute mathematical identity guarantee + // as block-prologue intrinsic gas and execution overhead are not accounted for here) + // for the total gas allocated to registered entries, ensuring the 63/64 forwarding rule + // remains compatible with the total gas cap. + function forwardingRuleCompatibleUpperBoundGasCap(uint64 _cap) private pure returns (uint256) { + return uint256(_cap) * 63 / 64; + } + + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * VIEW FUNCTIONS + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + /// @notice Returns all registered functions in their current execution order. + /// @return targets An array of target contract addresses corresponding to each registered function. + /// @return selectors An array of function selectors corresponding to each registered function. + function getExecutions() external view returns (address[] memory targets, bytes4[] memory selectors) { + uint256 len = executions.length; + targets = new address[](len); + selectors = new bytes4[](len); + + for (uint256 i = 0; i < len; i++) { + (address target, bytes4 selector) = unpackExecution(executions[i]); + + targets[i] = target; + selectors[i] = selector; + } + } + + /// @notice Returns the gas limit for a given (target, selector) pair. + /// @param _targetContract The target contract address. + /// @param _selector The function selector. + /// @return gasLimit Gas limit allocated for the function. + function getExecutionGasLimit(address _targetContract, bytes4 _selector) external view returns (uint64 gasLimit) { + uint256 executionEntry = packExecution(_targetContract, _selector); + uint256 index = findIndex(executionEntry); + gasLimit = uint64(executions[index]); + } + + /// @notice Returns all the registered target contracts. + /// @return targetContracts Array of addresses representing all registered target contracts. + function getTargetContracts() external view returns (address[] memory) { + uint256 len = executions.length; + address[] memory temp = new address[](len); + uint256 count; + + for (uint256 i = 0; i < len; i++) { + address targetContract = address(uint160(executions[i] >> 96)); + + bool exists; + for (uint256 j = 0; j < count; j++) { + if (temp[j] == targetContract) { + exists = true; + break; + } + } + + if (!exists) { + temp[count] = targetContract; + count += 1; + } + } + + address[] memory targetContracts = new address[](count); + for (uint256 i = 0; i < count; i++) { + targetContracts[i] = temp[i]; + } + + return targetContracts; + } + + /// @notice Returns all the selectors of a target contract. + /// @param _targetContract The target contract addresss. + /// @return selectors Array of function selectors registered for the target contract. + function getSelectors(address _targetContract) external view returns (bytes4[] memory) { + uint256 len = executions.length; + bytes4[] memory temp = new bytes4[](len); + uint256 count; + + for (uint256 i = 0; i < len; i++) { + (address target, bytes4 selector) = unpackExecution(executions[i]); + + if (target == _targetContract) { + temp[count] = selector; + count += 1; + } + } + + bytes4[] memory selectors = new bytes4[](count); + for (uint256 i = 0; i < count; i++) { + selectors[i] = temp[i]; + } + + return selectors; + } + + /// @notice Returns the target contract and selector at a given execution index. + /// @param _index The position in the execution order array. + /// @return target The target contract address. + /// @return selector The function selector to be called on the target. + function getExecutionAt(uint256 _index) external view returns (address target, bytes4 selector) { + require(_index < executions.length, InvalidIndex()); + + (target, selector) = unpackExecution(executions[_index]); + } + + /// @notice Returns the execution index for a given target contract and selector. + /// @param _targetContract The target contract address. + /// @param _selector The function selector registered for the target. + /// @return index The index in the execution order array. + function getExecutionIndex(address _targetContract, bytes4 _selector) external view returns (uint256 index) { + uint256 executionEntry = packExecution(_targetContract, _selector); + + return findIndex(executionEntry); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/src/Diamond.sol b/solidity/supra_contracts/src/Diamond.sol new file mode 100644 index 0000000000..939b2d6016 --- /dev/null +++ b/solidity/supra_contracts/src/Diamond.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +* +* Implementation of a diamond. +/******************************************************************************/ + +import {LibDiamond} from "./libraries/LibDiamond.sol"; +import {LibUtils} from "./libraries/LibUtils.sol"; +import {IDiamondCut} from "./interfaces/IDiamondCut.sol"; +import {IFacetSelectors} from "./interfaces/IFacetSelectors.sol"; +import {DiamondInit} from "./upgradeInitializers/DiamondInit.sol"; +import {FacetsDeployment, InitParams} from "./libraries/DiamondTypes.sol"; + +contract Diamond { + using LibUtils for address; + + /// @notice Constructor to initialize the diamond with owner and diamond cut facet. + /// @param _contractOwner The address of the contract owner. + /// @param _d Addresses of all deployed facets and DiamondInit. + /// @param _erc20Supra ERC20Supra contract address passed to DiamondInit. + /// @param _params Registry configuration passed to DiamondInit. + constructor( + address _contractOwner, + FacetsDeployment memory _d, + address _erc20Supra, + InitParams memory _params + ) { + LibDiamond.setContractOwner(_contractOwner); + + _d.configFacet.validateContractAddress(); + _d.coreFacet.validateContractAddress(); + _d.diamondCutFacet.validateContractAddress(); + _d.registryFacet.validateContractAddress(); + _d.ownershipFacet.validateContractAddress(); + _d.loupeFacet.validateContractAddress(); + _d.diamondInit.validateContractAddress(); + + // ------------------------------------------------------------------ + // Build the full cut array: + // slot 0 — diamondCut function (from DiamondCutFacet) + // slots 1-5 — remaining facets, each self-reporting their selectors + // ------------------------------------------------------------------ + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](6); + + bytes4[] memory cutSelectors = new bytes4[](1); + cutSelectors[0] = IDiamondCut.diamondCut.selector; + cut[0] = IDiamondCut.FacetCut({ + facetAddress: _d.diamondCutFacet, + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: cutSelectors + }); + + address[5] memory facets = [ + _d.loupeFacet, + _d.ownershipFacet, + _d.configFacet, + _d.registryFacet, + _d.coreFacet + ]; + for (uint256 i = 0; i < 5; i++) { + cut[i + 1] = IDiamondCut.FacetCut({ + facetAddress: facets[i], + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: IFacetSelectors(facets[i]).getSelectors() + }); + } + + // ------------------------------------------------------------------ + // Encode DiamondInit.init calldata and apply all cuts atomically + // ------------------------------------------------------------------ + bytes memory initCalldata = abi.encodeCall( + DiamondInit.init, + ( + _params, + _erc20Supra + ) + ); + + LibDiamond.diamondCut(cut, _d.diamondInit, initCalldata); + } + + /// @notice Find facet for function that is called and execute the + /// function if a facet is found and return any value. + fallback() external { + LibDiamond.DiamondStorage storage ds; + bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; + // get diamond storage + assembly { + ds.slot := position + } + // get facet from function selector + address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; + if (facet == address(0)) { revert LibDiamond.FunctionDoesNotExist(); } + // Execute external function from facet using delegatecall and return any value. + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the facet + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } +} diff --git a/solidity/supra_contracts/src/ERC20Supra.sol b/solidity/supra_contracts/src/ERC20Supra.sol new file mode 100644 index 0000000000..f7f78f5138 --- /dev/null +++ b/solidity/supra_contracts/src/ERC20Supra.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {IERC20Supra} from "../src/interfaces/IERC20Supra.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract ERC20Supra is ERC20Upgradeable, ERC20PermitUpgradeable, IERC20Supra, OwnableUpgradeable, UUPSUpgradeable { + using LibUtils for address; + + /// @notice Mapping of addresses authorized to mint and burn tokens. + mapping(address => bool) public authorizedAddresses; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * CONSTRUCTOR AND INITIALIZER + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the ERC20Supra token contract. + /// @param _initialOwner Address that will be assigned ownership of the contract. + /// @param _authorizedAddresses Array of addresses authorized to mint and burn tokens. + function initialize(address _initialOwner, address[] memory _authorizedAddresses) public initializer { + _initialOwner.validateAddress(); + + __ERC20_init("ERC20Supra", "SUPRA"); + __Ownable_init(_initialOwner); + __ERC20Permit_init("ERC20Supra"); + + uint256 len = _authorizedAddresses.length; + for (uint256 i = 0; i < len; i++) { + address addr = _authorizedAddresses[i]; + + addr.validateAddress(); + if (!authorizedAddresses[addr]) { authorizedAddresses[addr] = true; } + } + + emit InitializedAuthorizedAddresses(_authorizedAddresses); + } + + /// @notice Mints ERC20Supra tokens to a specified address. + /// @dev Can only be called by authorized addresses. + /// @param _to Address receiving the minted tokens. + /// @param _amount Amount of tokens to mint. + function mint(address _to, uint256 _amount) external { + isAuthorized(); + _mint(_to, _amount); + } + + /// @notice Burns ERC20Supra tokens from the caller. + /// @dev Can only be called by authorized addresses. + /// @param _amount Amount of tokens to burn. + function burn(uint256 _amount) external { + isAuthorized(); + _burn(msg.sender, _amount); + } + + /// @notice Burns ERC20Supra tokens from a specified address. + /// @dev Can only be called by authorized addresses. + /// @param _from Address whose tokens will be burned. + /// @param _amount Amount of tokens to burn. + function burnFrom(address _from, uint256 _amount) external { + isAuthorized(); + _burn(_from, _amount); + } + + /// @notice Adds an address to the authorization whitelist. + /// @dev Can only be called by the owner. + /// @param _addr Address to authorize. + function addAuthorizedAddress(address _addr) external onlyOwner { + _addr.validateAddress(); + if (authorizedAddresses[_addr]) revert AddressAlreadyAuthorized(); + + authorizedAddresses[_addr] = true; + emit AuthorizedAddressAdded(_addr, msg.sender); + } + + /// @notice Removes an address from the authorization whitelist. + /// @dev Can only be called by the owner. + /// @param _addr Address to deauthorize. + function removeAuthorizedAddress(address _addr) external onlyOwner { + require(authorizedAddresses[_addr], AddressNotAuthorized()); + + delete authorizedAddresses[_addr]; + emit AuthorizedAddressRemoved(_addr, msg.sender); + } + + /// @notice Checks whether the caller is authorized to mint or burn tokens. + /// @dev Reverts if the caller is not in the authorized whitelist. + function isAuthorized() private view { + require(authorizedAddresses[msg.sender], UnauthorizedCaller()); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/src/ERC20SupraHandler.sol b/solidity/supra_contracts/src/ERC20SupraHandler.sol new file mode 100644 index 0000000000..e41021760b --- /dev/null +++ b/solidity/supra_contracts/src/ERC20SupraHandler.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {IERC20Supra} from "../src/interfaces/IERC20Supra.sol"; +import {IERC20SupraHandler} from "../src/interfaces/IERC20SupraHandler.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract ERC20SupraHandler is OwnableUpgradeable, UUPSUpgradeable, IERC20SupraHandler { + using LibUtils for address; + + /// @notice Address of the ERC20Supra contract. + address public erc20Supra; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * CONSTRUCTOR AND INITIALIZER + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the owner of the contract and address of the ERC20Supra. + function initialize(address _initialOwner, address _erc20Supra) public initializer { + __Ownable_init(_initialOwner); + + _erc20Supra.validateContractAddress(); + erc20Supra = _erc20Supra; + } + + /// @notice Deposit native token → Mint ERC20Supra 1:1 + function deposit() public payable { + if (msg.value == 0) revert InvalidAmount(); + IERC20Supra(erc20Supra).mint(msg.sender, msg.value); + + emit Deposit(msg.sender, msg.value); + } + + /// @notice Withdraw native token → Burn ERC20Supra 1:1 + /// @param _amount Amount of native tokens to withdraw. + function withdraw(uint256 _amount) external { + if (_amount == 0) revert InvalidAmount(); + if (IERC20(erc20Supra).balanceOf(msg.sender) < _amount) revert InsufficientBalance(); + if (address(this).balance < _amount) revert InsufficientContractBalance(); + + IERC20Supra(erc20Supra).burnFrom(msg.sender, _amount); + emit Withdrawal(msg.sender, _amount); + + (bool sent, ) = payable(msg.sender).call{value: _amount}(""); + if (!sent) revert TransferFailed(); + } + + /// @notice Allows a user to send native tokens directly and get ERC20Supra. + receive() external payable { + deposit(); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/src/MultiSignatureWallet.sol b/solidity/supra_contracts/src/MultiSignatureWallet.sol new file mode 100644 index 0000000000..7db2060eb7 --- /dev/null +++ b/solidity/supra_contracts/src/MultiSignatureWallet.sol @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {IMultiSignatureWallet} from "./interfaces/IMultiSignatureWallet.sol"; + +/** + * @title MultiSignatureWallet + * @dev A multisignature wallet contract that requires multiple owners to confirm transactions. + */ +contract MultiSignatureWallet is Initializable, IMultiSignatureWallet { + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet private owners; + uint256 public numConfirmationsRequired; + + /// @dev Default cap on submitTransaction's timeout duration, seeded at initialize() and + /// adjustable afterwards via updateMaxTimeoutDuration. + uint64 private constant DEFAULT_MAX_TIMEOUT_DURATION = 30 days; + + /// @notice Maximum timeout duration, in seconds, allowed for a newly submitted transaction. + uint64 public maxTimeoutDuration; + + // Structure to hold transaction details + struct Transaction { + address to; // Transaction target address + uint64 timeout; // Expiry timestamp of the transaction + uint256 value; // Amount of ether sent with the transaction + bytes data; // Data payload of the transaction + } + + // Mapping to track confirmations for each transaction. + mapping(uint256 => EnumerableSet.AddressSet) private confirmations; + + // Mapping from transaction index to Transaction + mapping(uint256 => Transaction) private transactions; + + // Auto-incrementing transaction index + uint256 private txIndex; + + // Number of active transactions + uint256 public txCount; + + /// @dev Bumped whenever numConfirmationsRequired is lowered. A confirmation is only valid if + /// it was stamped under the current value (see confirmationTxEpoch), so a threshold + /// decrease can never retroactively satisfy a transaction that fell short of the old, + /// higher threshold - every owner (including the original submitter) must re-confirm. + /// Deliberately a plain counter compared live, rather than a bulk "clear all existing + /// confirmations" step: Solidity's `delete` cannot clear a nested mapping (like + /// EnumerableSet's internal position-tracking mapping), so a bulk clear of an + /// still-in-use confirmation set would leave stale membership behind. Comparing + /// per-confirmation stamps against this live counter sidesteps that entirely. + uint32 private currentEpoch; + + /// @dev Per-owner "incarnation" counter, bumped once whenever that address is removed via + /// removeOwners. A confirmation is only valid if it was stamped under the owner's + /// current incarnation (see ownerConfirmationEpoch), so if a removed owner is later + /// re-added, any confirmation they left behind before removal no longer counts. + mapping(address => uint32) private ownerEpoch; + + /// @dev Records, per (txIndex, owner), the ownerEpoch value the owner's confirmation was + /// stamped under. Compared against the owner's current ownerEpoch to decide validity. + mapping(uint256 => mapping(address => uint32)) private ownerConfirmationEpoch; + + /// @dev Records, per (txIndex, owner), the currentEpoch value the owner's confirmation was + /// stamped under. Compared against the live currentEpoch to decide validity. + mapping(uint256 => mapping(address => uint32)) private confirmationTxEpoch; + + // Function to ensure the caller is an owner + function onlyOwner(address owner) private view { + if (!owners.contains(owner)) + revert NotAnOwner(); + } + + // Function to ensure the caller is the multisig contract itself + function onlyMultiSig() private view { + if (msg.sender != address(this)) { + revert OnlyMultisigAccountCanCall(); + } + } + + // Function to check if a transaction exists + function txExists(uint256 _txIndex) private view { + if (transactions[_txIndex].to == address(0)) + revert InvalidTxnId(); + } + + /// @dev Reverts if the transaction's timeout has passed but it hasn't been swept from storage + /// yet. View-only: never mutates state. Actual cleanup happens via + /// removeExpiredTransaction. Uses a distinct error from txExists so callers can tell an + /// expired transaction apart from one that never existed. + function notExpired(uint256 _txIndex) private view { + if (transactions[_txIndex].timeout < block.timestamp) revert TransactionAlreadyExpired(); + } + + /// @dev Whether owner's confirmation on _txIndex is still valid: they must still be a member + /// of the confirmation set, their confirmation must have been stamped under their + /// current owner-incarnation, AND it must have been stamped under the current + /// threshold-epoch. The membership check must come first: a never-confirmed owner has + /// both epoch mappings defaulting to 0, which would otherwise spuriously read as valid + /// whenever ownerEpoch/currentEpoch also happen to still be 0. + function _isOwnerConfirmationValid(uint256 _txIndex, address owner) private view returns (bool) { + return confirmations[_txIndex].contains(owner) && + ownerConfirmationEpoch[_txIndex][owner] == ownerEpoch[owner] && + confirmationTxEpoch[_txIndex][owner] == currentEpoch; + } + + /// @dev Counts confirmations from current valid owners only. + /// @param _txIndex Index of the transaction to count confirmations for. + /// @return uint24 Number of confirmations from current valid owners. + function validNumberOfConfirmations(uint256 _txIndex) private view returns (uint24) { + EnumerableSet.AddressSet storage confirmation = confirmations[_txIndex]; + uint24 validNumOfConfirmations = 0; + for (uint64 i = 0; i < confirmation.length(); i++) { + address owner = confirmation.at(i); + if (owners.contains(owner) && _isOwnerConfirmationValid(_txIndex, owner)) { + validNumOfConfirmations++; + } + } + return validNumOfConfirmations; + } + + /// @dev Helper function to remove a transaction from storage. + /// @param _txIndex Index of the transaction to remove. + function removeTransaction(uint256 _txIndex) private { + // Remove the transaction from storage + delete transactions[_txIndex]; + + // Remove confirmations mapping + delete confirmations[_txIndex]; + + txCount--; + } + + // Function to check if a transaction has not been confirmed by the caller + function notConfirmed(uint256 _txIndex) private view { + if (_isOwnerConfirmationValid(_txIndex, msg.sender)) revert TxnAlreadyConfirmed(); + } + + /// @dev Records owner's confirmation of _txIndex, stamping both epochs current at the time of + /// confirmation. Shared by submitTransaction's implicit self-confirmation and + /// confirmTransaction so the two paths can't drift apart. + function _recordConfirmation(uint256 _txIndex, address owner) private { + confirmations[_txIndex].add(owner); + ownerConfirmationEpoch[_txIndex][owner] = ownerEpoch[owner]; + confirmationTxEpoch[_txIndex][owner] = currentEpoch; + } + + /** + * @dev Disables the initialization for the implementation contract. + */ + constructor() { + _disableInitializers(); + } + + /** + * @dev Initializes the contract with initial owners and required confirmations. + * @param _owners Array of initial owner addresses. + * @param _numConfirmationsRequired Number of confirmations required for transactions. + */ + function initialize(address[] memory _owners, uint256 _numConfirmationsRequired) public initializer { + if (_owners.length == 0) revert OwnersRequired(); + if ( + _numConfirmationsRequired == 0 || + _numConfirmationsRequired > _owners.length + ) revert InvalidNumberOfConfirmations(); + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + if (owner == address(0)) revert InvalidOwner(); + require(owners.add(owner), OwnerNotUnique()); + } + + numConfirmationsRequired = _numConfirmationsRequired; + maxTimeoutDuration = DEFAULT_MAX_TIMEOUT_DURATION; + } + + /** + * @dev Fallback function to receive ether and emit a deposit event. + */ + receive() external payable { + emit Deposit(msg.sender, msg.value, address(this).balance); + } + + /** + * @dev Function to submit a new transaction to the wallet. + * @param _to Address of the contract the transaction is directed to. + * @param _value Amount of ether to be sent with the transaction. + * @param _timeoutDuration Duration after which the transaction will get expire. + * @param _data Data payload of the transaction. + */ + function submitTransaction( + address _to, + uint256 _value, + uint64 _timeoutDuration, + bytes memory _data + ) external payable { + onlyOwner(msg.sender); + if (_to == address(0)) revert InvalidRecipient(); + if (_timeoutDuration > maxTimeoutDuration) revert TimeoutTooLong(); + + uint256 currentTxIndex = txIndex; + + transactions[currentTxIndex] = Transaction({ + to: _to, + timeout: uint64(block.timestamp) + _timeoutDuration, + value: _value, + data: _data + }); + + //We assume the act of submission is an implicit confirmation + _recordConfirmation(currentTxIndex, msg.sender); + txIndex++; + txCount++; + + emit SubmitTransaction(msg.sender, currentTxIndex, _to, _value, _data); + } + + /** + * @dev Function to confirm an existing transaction. + * @dev Reverts if the transaction has expired; call removeExpiredTransaction to clean it up. + * @param _txIndex Index of the transaction to confirm. + */ + function confirmTransaction(uint256 _txIndex) public { + onlyOwner(msg.sender); + txExists(_txIndex); + notExpired(_txIndex); + notConfirmed(_txIndex); + + _recordConfirmation(_txIndex, msg.sender); + + emit ConfirmTransaction(msg.sender, _txIndex); + } + + /** + * @dev Function to execute a confirmed transaction. + * @dev Reverts if the transaction has expired; call removeExpiredTransaction to clean it up. + * @param _txIndex Index of the transaction to execute. + */ + function executeTransaction(uint256 _txIndex) public returns (bytes memory) { + onlyOwner(msg.sender); + txExists(_txIndex); + notExpired(_txIndex); + + Transaction memory transaction = transactions[_txIndex]; + if (validNumberOfConfirmations(_txIndex) < numConfirmationsRequired) + revert NotEnoughConfirmation(); + + removeTransaction(_txIndex); + + (bool success, bytes memory data) = transaction.to.call{value: transaction.value}(transaction.data); + if (!success) { revert ExecutionFailed(); } + + emit ExecuteTransaction(msg.sender, _txIndex, data); + return data; + } + + /** + * @dev Function to revoke a previously given confirmation for a transaction. + * @dev Reverts if the transaction has expired; call removeExpiredTransaction to clean it up. + * @param _txIndex Index of the transaction to revoke confirmation. + */ + function revokeConfirmation(uint256 _txIndex) external { + onlyOwner(msg.sender); + txExists(_txIndex); + notExpired(_txIndex); + // Gated on raw membership rather than _isOwnerConfirmationValid: an owner whose + // confirmation went stale (threshold lowered, or removed and re-added) is no longer + // counted anywhere, but should still be able to clear their own now-inert set entry + // instead of being permanently stuck as an unrevocable member. + if (!confirmations[_txIndex].contains(msg.sender)) revert TransactionNotConfirmed(); + + confirmations[_txIndex].remove(msg.sender); + delete ownerConfirmationEpoch[_txIndex][msg.sender]; + delete confirmationTxEpoch[_txIndex][msg.sender]; + + emit RevokeConfirmation(msg.sender, _txIndex); + } + + /** + * @dev Permissionlessly removes a transaction whose timeout has passed. Its only effect is + * discarding an already-worthless transaction, so no access control is needed - anyone + * (e.g. an ops keeper) can call this to garbage-collect. + * @param _txIndex Index of the expired transaction to remove. + */ + function removeExpiredTransaction(uint256 _txIndex) external { + txExists(_txIndex); + if (transactions[_txIndex].timeout >= block.timestamp) revert NotExpired(); + + removeTransaction(_txIndex); + emit TransactionExpired(_txIndex); + } + + /** + * @dev Function to cancel a pending transaction that is stuck or no longer wanted. Reachable + * only through the multisig's own submit/confirm/execute flow. + * @param _txIndex Index of the transaction to cancel. + */ + function cancelTransaction(uint256 _txIndex) external { + onlyMultiSig(); + txExists(_txIndex); + + removeTransaction(_txIndex); + emit TransactionCancelled(_txIndex); + } + + /** + * @dev Function to add new owners to the wallet. + * @param _owners Array of new owner addresses to be added. + */ + function addOwners(address[] memory _owners) external { + onlyMultiSig(); + if (_owners.length == 0) revert OwnersRequired(); + + address[] memory ownersToUpdate = new address[](_owners.length); + uint256 c = 0; + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + if (owner == address(0)) revert InvalidOwner(); + if (owners.add(owner)) { + ownersToUpdate[c++] = owner; + } + } + if (c > 0) { + // Trim the array to the addresses actually written before emitting, so the event + // doesn't carry trailing address(0) entries for no-op inputs. + assembly ("memory-safe") { + mstore(ownersToUpdate, c) + } + emit OwnersAdded(ownersToUpdate); + } + } + + /** + * @dev Function to remove existing owners from the wallet. + * @dev Bumps ownerEpoch for each removed address so that any confirmation they left behind + * on a still-pending transaction stops counting immediately. A later re-add via addOwners + * does not restore it - see ownerEpoch. + * @param _owners Array of existing owner addresses to be removed. + */ + function removeOwners(address[] memory _owners) external { + onlyMultiSig(); + if (_owners.length == 0) revert OwnersRequired(); + address[] memory ownersToUpdate = new address[](_owners.length); + uint256 c = 0; + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + if (owners.remove(owner)) { + ownerEpoch[owner]++; + ownersToUpdate[c++] = owner; + } + } + + if (owners.length() < numConfirmationsRequired) { + revert InvalidNumberOfConfirmations(); + } + + if (c > 0) { + assembly ("memory-safe") { + mstore(ownersToUpdate, c) + } + emit OwnersRemoved(ownersToUpdate); + } + } + + /** + * @dev Function to update the number of required confirmations for transactions. + * @dev Lowering the threshold bumps currentEpoch, invalidating confirmations on every + * pending transaction system-wide, so a decrease can never retroactively satisfy a + * transaction that fell short of the old, higher threshold. Raising the threshold is + * self-enforcing via the live comparison in hasValidNumberOfConfirmations and does not + * need invalidation. + * @param _numConfirmationsRequired New number of confirmations required for transactions. + */ + function updateNumConfirmations(uint256 _numConfirmationsRequired) external { + onlyMultiSig(); + if ( + _numConfirmationsRequired == 0 || + _numConfirmationsRequired > owners.length() + ) revert InvalidNumberOfConfirmations(); + + if (_numConfirmationsRequired < numConfirmationsRequired) { + currentEpoch++; + } + + numConfirmationsRequired = _numConfirmationsRequired; + emit NumConfirmationUpdated(_numConfirmationsRequired); + } + + /** + * @dev Function to update the maximum timeout duration allowed for newly submitted + * transactions. Only affects transactions submitted after the update. + * @param _newMaxTimeoutDuration New maximum timeout duration, in seconds. + */ + function updateMaxTimeoutDuration(uint64 _newMaxTimeoutDuration) external { + onlyMultiSig(); + if (_newMaxTimeoutDuration == 0) revert InvalidMaxTimeoutDuration(); + + maxTimeoutDuration = _newMaxTimeoutDuration; + emit MaxTimeoutDurationUpdated(_newMaxTimeoutDuration); + } + + /** + * @dev Function to retrieve the list of current owners of the wallet. + * @return Array of addresses representing the current owners. + */ + function getOwners() public view returns (address[] memory) { + return owners.values(); + } + + /** + * @dev Function to retrieve the potential index of the next transaction. + * @return Index of the next transaction of uint256 type. + */ + function getNextTransactionIndex() public view returns (uint256) { + return txIndex; + } + + /** + * @dev Checks if a transaction is confirmed by an owner. + * @param _txIndex Index of the transaction to check for. + * @param _owner Address of the owner. + */ + function isConfirmed(uint256 _txIndex, address _owner) external view returns (bool) { + onlyOwner(_owner); + txExists(_txIndex); + notExpired(_txIndex); + return _isOwnerConfirmationValid(_txIndex, _owner); + } + + /** + * @dev Function to retrieve details of a specific transaction. + * @param _txIndex Index of the transaction to retrieve details for. + * @return to Transaction target address. + * @return value Amount of ether sent with the transaction. + * @return numConfirmations Number of confirmations received for the transaction. + * @return timeout Expiry timestamp of the transaction. + * @return data Data payload of the transaction. + */ + function getTransaction( + uint256 _txIndex + ) + public + view + returns ( + address to, + uint256 value, + uint24 numConfirmations, + uint64 timeout, + bytes memory data + ) + { + txExists(_txIndex); + notExpired(_txIndex); + Transaction storage transaction = transactions[_txIndex]; + + return ( + transaction.to, + transaction.value, + validNumberOfConfirmations(_txIndex), + transaction.timeout, + transaction.data + ); + } + + /** + * @notice Deploys a contract using raw CREATE opcode + * @param _creationCode The creation bytecode of the contract to deploy + * @param _value Amount of ETH to sent along with contract creation. + * @return deployed The address of the deployed contract + */ + function deployContract(bytes memory _creationCode, uint256 _value) external returns (address deployed) { + onlyMultiSig(); + if (_creationCode.length == 0) { revert EmptyCreationCode(); } + + assembly ("memory-safe") { + // CREATE(value, offset, size) + deployed := create( + _value, // forward ETH if any + add(_creationCode, 0x20), // skip the length slot + mload(_creationCode) // size of creation code + ) + } + if (deployed == address(0)) { revert ContractCreationFailed(); } + emit ContractDeployed(deployed); + } + + /** + * @dev Function to check if a transaction has a valid number of confirmations. + * @param _txIndex Index of the transaction to check for. + * @return bool True if the transaction has a valid number of confirmations counting only valid owners, false otherwise. + */ + function hasValidNumberOfConfirmations(uint256 _txIndex) public view returns (bool) { + txExists(_txIndex); + notExpired(_txIndex); + return validNumberOfConfirmations(_txIndex) >= numConfirmationsRequired; + } +} diff --git a/solidity/supra_contracts/src/MultisigBeacon.sol b/solidity/supra_contracts/src/MultisigBeacon.sol new file mode 100644 index 0000000000..41759be6be --- /dev/null +++ b/solidity/supra_contracts/src/MultisigBeacon.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +/** + * @title MultisigBeacon + * @dev A beacon that stores the implementation address for multisig proxies. + * Admin can upgrade the implementation to a new version. + * + * Uses two-step ownership transfer (Ownable2Step): transferOwnership only designates a + * pending owner, who must separately call acceptOwnership to complete the transfer, so the + * current owner keeps control of the beacon until that happens. renounceOwnership is + * disabled entirely, since giving up ownership here would leave upgradeTo permanently + * unreachable. + */ +contract MultisigBeacon is UpgradeableBeacon, Ownable2Step { + /// @notice Thrown by renounceOwnership, which is always disabled on this contract. + error OwnershipRenunciationDisabled(); + + /** + * @dev Constructor to initialize the addresses for implementation and initial owner. + * @param _implementation Address of the initial multisig implementation contract. + * @param _owner Address of the Beacon owner. + */ + constructor(address _implementation, address _owner) UpgradeableBeacon(_implementation, _owner) {} + + /// @dev Resolves the Ownable/Ownable2Step diamond by forwarding to the two-step implementation. + function transferOwnership(address newOwner) public override(Ownable, Ownable2Step) { + Ownable2Step.transferOwnership(newOwner); + } + + /// @dev Resolves the Ownable/Ownable2Step diamond by forwarding to the two-step implementation. + function _transferOwnership(address newOwner) internal override(Ownable, Ownable2Step) { + Ownable2Step._transferOwnership(newOwner); + } + + /// @dev Always reverts - see contract-level NatSpec for why renouncing is disabled. + function renounceOwnership() public view override onlyOwner { + revert OwnershipRenunciationDisabled(); + } +} diff --git a/solidity/supra_contracts/src/SupraContractsBindings.sol b/solidity/supra_contracts/src/SupraContractsBindings.sol new file mode 100644 index 0000000000..38a531922e --- /dev/null +++ b/solidity/supra_contracts/src/SupraContractsBindings.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibCommon} from "./libraries/LibCommon.sol"; +import {TaskMetadata} from "./libraries/LibAppStorage.sol"; + +interface SupraContractsBindings { + + // View function of Automation Registry Diamond + function isInitialized() external view returns (bool); + + // View functions of RegistryFacet + function ifTaskExists(uint64 _taskIndex) external view returns (bool); + function getActiveTaskIds() external view returns (uint256[] memory); + function getTaskIdList() external view returns (uint256[] memory); + function getTaskDetails(uint64 _taskIndex) external view returns (TaskMetadata memory); + function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (TaskMetadata[] memory); + + // View functions of CoreFacet + function isAutomationReadyEnabled() external view returns (bool); + function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory); + + // Entry function to be called by node runtime for bookkeeping + function processTasks(uint64 _cycleIndex, uint256[] memory _taskIndexes) external; + + // Entry function to be called by node runtime to remove tasks with fatal errors + function removeRegisteredTask(uint64 _cycleIndex, uint64 _taskIndex, string memory _reason) external; + + // Entry function of the BlockMeta for block metadata transaction + function blockPrologue() external; + + // Emitted when the cycle state transitions. + event AutomationCycleEvent( + uint64 indexed index, + LibCommon.CycleState indexed state, + uint64 startTime, + uint64 durationSecs, + LibCommon.CycleState indexed oldState + ); +} diff --git a/solidity/supra_contracts/src/facets/ConfigFacet.sol b/solidity/supra_contracts/src/facets/ConfigFacet.sol new file mode 100644 index 0000000000..f6436b574f --- /dev/null +++ b/solidity/supra_contracts/src/facets/ConfigFacet.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {AppStorage, Config, RegistryState, LibAppStorage} from "../libraries/LibAppStorage.sol"; +import {LibCommon} from "../libraries/LibCommon.sol"; +import {LibUtils} from "../libraries/LibUtils.sol"; +import {IConfigFacet} from "../interfaces/IConfigFacet.sol"; +import {IFacetSelectors} from "../interfaces/IFacetSelectors.sol"; +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract ConfigFacet is IConfigFacet, IFacetSelectors { + using EnumerableSet for *; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ADMIN FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Grants authorization to the input account to submit system automation tasks. + /// It is foundation governance responsibility to make sure that the target is and instance of `MultiSignatureWallet` + /// @param _account Address to grant authorization to. + function grantAuthorization(address _account) external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + require(s.authorizedAccounts.add(_account), AddressAlreadyExists()); + emit AuthorizationGranted(_account, block.timestamp); + } + + /// @notice Revokes authorization from the input account to submit system automation tasks. + /// @param _account Address to revoke authorization from. + function revokeAuthorization(address _account) external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + require(s.authorizedAccounts.remove(_account), AddressDoesNotExist()); + emit AuthorizationRevoked(_account, block.timestamp); + } + + /// @notice Function to enable the task registration. + function enableRegistration() external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + if (s.registrationEnabled) { revert AlreadyEnabled(); } + s.registrationEnabled = true; + + emit TaskRegistrationEnabled(s.registrationEnabled); + } + + /// @notice Function to disable the task registration. + function disableRegistration() external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + if (!s.registrationEnabled) { revert AlreadyDisabled(); } + s.registrationEnabled = false; + + emit TaskRegistrationDisabled(s.registrationEnabled); + } + + /// @notice Function to withdraw the accumulated fees. + /// @param _amount Amount to withdraw. + /// @param _recipient Address to withdraw fees to. + function withdrawFees(uint256 _amount, address _recipient) external { + LibDiamond.enforceIsContractOwner(); + + if (_amount == 0) { revert InvalidAmount(); } + LibUtils.validateAddress(_recipient); + AppStorage storage s = LibAppStorage.appStorage(); + uint256 balance = IERC20(s.erc20Supra).balanceOf(address(this)); + + if (balance < _amount) { revert InsufficientBalance(); } + + RegistryState storage registryState = LibAppStorage.registryState(); + if (balance - _amount < registryState.cycleLockedFees + registryState.totalDepositedAutomationFees) { revert RequestExceedsLockedBalance(); } + + bool sent = IERC20(s.erc20Supra).transfer(_recipient, _amount); + if (!sent) { revert TransferFailed(); } + + emit RegistryFeeWithdrawn(_recipient, _amount); + } + + /// @notice Function to update the registry configuration buffer. + /// @dev `_taskCapacity` and `_sysTaskCapacity` bound how many tasks `CoreFacet.monitorCycleEnd()` + /// iterates over each cycle, so raising them raises that function's per-call gas cost. + /// `monitorCycleEnd()` is invoked once per block as a single registered entry in the + /// separate `BlockMeta` contract's `blockPrologue()` dispatch loop, under the fixed + /// per-entry gas limit it was registered with there (see `BlockMeta.getExecutionGasLimit`) + /// and `BlockMeta`'s own `blockPrologueGasCap`. This contract has no on-chain reference to + /// `BlockMeta` and intentionally does not cap `_taskCapacity`/`_sysTaskCapacity` against it, + /// so task-capacity growth stays possible without an upgrade here. That means increasing + /// these values is NOT automatically safe: before calling this with a higher capacity, + /// verify off-chain that the resulting worst-case `monitorCycleEnd()` gas cost still fits + /// under `BlockMeta`'s registered gas limit for it, or `blockPrologue()` will start + /// reverting/OOG-ing on that entry every block. + function updateConfigBuffer( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint8 _congestionThresholdPercentage, + uint128 _congestionBaseFeeWeiPerSec, + uint8 _congestionExponent, + uint8 _maxCongestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + LibCommon.validateConfigParameters( + _taskDurationCapSecs, + _registryMaxGasCap, + _congestionThresholdPercentage, + _congestionExponent, + _maxCongestionExponent, + _taskCapacity, + _cycleDurationSecs, + _sysTaskDurationCapSecs, + _sysRegistryMaxGasCap, + _sysTaskCapacity + ); + + RegistryState storage registryState = LibAppStorage.registryState(); + if (registryState.gasCommittedForNextCycle > _registryMaxGasCap) { revert UnacceptableRegistryMaxGasCap(); } + if (registryState.sysGasCommittedForNextCycle > _sysRegistryMaxGasCap) { revert UnacceptableSysRegistryMaxGasCap(); } + + // Add new config to the buffer + Config memory configBuffer = Config({ + registryMaxGasCap: _registryMaxGasCap, + sysRegistryMaxGasCap: _sysRegistryMaxGasCap, + automationBaseFeeWeiPerSec: _automationBaseFeeWeiPerSec, + flatRegistrationFeeWei: _flatRegistrationFeeWei, + congestionBaseFeeWeiPerSec: _congestionBaseFeeWeiPerSec, + taskDurationCapSecs: _taskDurationCapSecs, + sysTaskDurationCapSecs: _sysTaskDurationCapSecs, + cycleDurationSecs: _cycleDurationSecs, + taskCapacity: _taskCapacity, + sysTaskCapacity: _sysTaskCapacity, + congestionThresholdPercentage: _congestionThresholdPercentage, + congestionExponent: _congestionExponent, + maxCongestionExponent: _maxCongestionExponent + }); + s.configuration[LibAppStorage.BUFFER_CONFIG] = configBuffer; + s.ifBufferExists = true; + + registryState.nextCycleRegistryMaxGasCap = _registryMaxGasCap; + registryState.nextCycleSysRegistryMaxGasCap = _sysRegistryMaxGasCap; + + emit ConfigBufferUpdated(configBuffer); + } + + /// @notice Updates the task-registration input size caps. Takes effect immediately (unlike + /// updateConfigBuffer) since these only gate new registrations, which are already blocked + /// outside cycle state STARTED, so there's no mid-cycle-fairness reason to defer them. + /// @param _maxPayloadLength Max length in bytes of a task's payloadTx. + /// @param _maxPredicateLength Max length in bytes of a task's predicate. + /// @param _maxAuxDataLength Max combined length in bytes across all of a task's auxData entries. + /// @param _maxAuxDataEntries Max number of entries in a task's auxData array, bounded + /// independently of _maxAuxDataLength. + function updateDataLengthCaps( + uint16 _maxPayloadLength, + uint16 _maxPredicateLength, + uint16 _maxAuxDataLength, + uint16 _maxAuxDataEntries + ) external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + s.maxPayloadLength = _maxPayloadLength; + s.maxPredicateLength = _maxPredicateLength; + s.maxAuxDataLength = _maxAuxDataLength; + s.maxAuxDataEntries = _maxAuxDataEntries; + + emit DataLengthCapsUpdated(_maxPayloadLength, _maxPredicateLength, _maxAuxDataLength, _maxAuxDataEntries); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the ERC20Supra address. + function erc20Supra() external view returns (address) { + return LibAppStorage.appStorage().erc20Supra; + } + + /// @notice Returns if task registration is enabled. + function isRegistrationEnabled() external view returns (bool) { + return LibAppStorage.appStorage().registrationEnabled; + } + + /// @notice Returns the registry configuration. + function getConfig() external view returns (Config memory) { + return LibAppStorage.activeConfig(); + } + + /// @notice Returns the pending configuration. + function getConfigBuffer() external view returns (Config memory) { + return LibAppStorage.bufferConfig(); + } + + /// @notice Returns the current task-registration input size caps. + function getDataLengthCaps() external view returns (uint16 maxPayloadLength, uint16 maxPredicateLength, uint16 maxAuxDataLength, uint16 maxAuxDataEntries) { + AppStorage storage s = LibAppStorage.appStorage(); + maxPayloadLength = s.maxPayloadLength; + maxPredicateLength = s.maxPredicateLength; + maxAuxDataLength = s.maxAuxDataLength; + maxAuxDataEntries = s.maxAuxDataEntries; + } + + function getSelectors() external pure override returns (bytes4[] memory selectors) { + selectors = new bytes4[](12); + selectors[0] = ConfigFacet.grantAuthorization.selector; + selectors[1] = ConfigFacet.revokeAuthorization.selector; + selectors[2] = ConfigFacet.enableRegistration.selector; + selectors[3] = ConfigFacet.disableRegistration.selector; + selectors[4] = ConfigFacet.withdrawFees.selector; + selectors[5] = ConfigFacet.updateConfigBuffer.selector; + selectors[6] = ConfigFacet.erc20Supra.selector; + selectors[7] = ConfigFacet.isRegistrationEnabled.selector; + selectors[8] = ConfigFacet.getConfig.selector; + selectors[9] = ConfigFacet.getConfigBuffer.selector; + selectors[10] = ConfigFacet.updateDataLengthCaps.selector; + selectors[11] = ConfigFacet.getDataLengthCaps.selector; + } +} diff --git a/solidity/supra_contracts/src/facets/CoreFacet.sol b/solidity/supra_contracts/src/facets/CoreFacet.sol new file mode 100644 index 0000000000..7bae344c82 --- /dev/null +++ b/solidity/supra_contracts/src/facets/CoreFacet.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {AppStorage, LibAppStorage, TransitionState} from "../libraries/LibAppStorage.sol"; +import {LibCommon} from "../libraries/LibCommon.sol"; +import {LibCore} from "../libraries/LibCore.sol"; +import {LibUtils} from "../libraries/LibUtils.sol"; +import {ICoreFacet} from "../interfaces/ICoreFacet.sol"; +import {IFacetSelectors} from "../interfaces/IFacetSelectors.sol"; +import {IRegistryStatus} from "../interfaces/IRegistryStatus.sol"; +import {LibDiamond} from "../libraries/LibDiamond.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract CoreFacet is ICoreFacet, IFacetSelectors { + using LibUtils for address; + using EnumerableSet for EnumerableSet.UintSet; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VM FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Called by the VM Signer on `AutomationBookkeepingAction::Process` action emitted by native layer ahead of the cycle transition. + /// @dev The node's off-chain VM-signer decoder hardcodes this function's selector and calls + /// it every cycle transition. Do not Remove this selector via diamondCut post-genesis; + /// Replace (to ship a fix) is fine. + /// @param _cycleIndex Index of the cycle. + /// @param _taskIndexes Array of task index to be processed. + function processTasks(uint64 _cycleIndex, uint256[] memory _taskIndexes) external { + // Check caller is VM Signer + msg.sender.enforceIsVmSigner(); + + if (_taskIndexes.length == 0) { return; } + + AppStorage storage s = LibAppStorage.appStorage(); + LibCommon.CycleState state = s.cycleState; + if (state == LibCommon.CycleState.FINISHED) { + LibCore.onCycleTransition(_cycleIndex, _taskIndexes); + } else { + if (state != LibCommon.CycleState.SUSPENDED) { revert InvalidRegistryState(); } + LibCore.onCycleSuspend(_cycleIndex, _taskIndexes); + } + } + + /// @notice Checks the cycle end and emit an event on it. Does nothing if cycle is not in `STARTED` state. + /// @dev The node's off-chain VM-signer decoder hardcodes this function's selector and calls + /// it every block. Do not Remove this selector via diamondCut post-genesis; Replace (to + /// ship a fix) is fine. + function monitorCycleEnd() external { + tx.origin.enforceIsVmSigner(); + + if (!LibCommon.isCycleStarted() || LibCommon.getCycleEndTime() > block.timestamp) { + return; + } + + LibCore.onCycleEndInternal(); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ADMIN FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function to enable the automation. + function enableAutomation() external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + if (s.automationEnabled) { revert AlreadyEnabled(); } + + s.automationEnabled = true; + if (s.cycleState == LibCommon.CycleState.READY) { + LibCore.updateConfigFromBuffer(); + LibCore.moveToStartedState(); + } + + emit AutomationEnabled(s.automationEnabled); + } + + /// @notice Function to disable the automation. + function disableAutomation() external { + LibDiamond.enforceIsContractOwner(); + + AppStorage storage s = LibAppStorage.appStorage(); + if (!s.automationEnabled) { revert AlreadyDisabled(); } + + s.automationEnabled = false; + if (LibCommon.isCycleStarted() || (s.cycleState == LibCommon.CycleState.FINISHED && !LibCore.isTransitionInProgress())) { + LibCore.tryMoveToSuspendedState(); + } + emit AutomationDisabled(s.automationEnabled); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the index, start time, duration and state of the current cycle. + function getCycleInfo() external view returns (uint64, uint64, uint64, LibCommon.CycleState) { + AppStorage storage s = LibAppStorage.appStorage(); + return (s.index, s.startTime, s.durationSecs, s.cycleState); + } + + /// @notice Returns the duration of the current cycle. + function getCycleDuration() external view returns (uint64) { + AppStorage storage s = LibAppStorage.appStorage(); + return s.durationSecs; + } + + /// @notice Returns the refund duration and automation fee per sec of the transition state. + /// @return Refund duration + /// @return Automation fee per sec + function getTransitionInfo() external view returns (uint64, uint128) { + TransitionState storage transitionState = LibAppStorage.transitionState(); + return (transitionState.refundDuration, transitionState.automationFeePerSec); + } + + /// @notice Returns the index, start time, duration, state, transition details if any of the current cycle. + /// @dev Node's off-chain automation registry manager relies on existence of it. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory details) { + AppStorage storage s = LibAppStorage.appStorage(); + details.index = s.index; + details.startTime = s.startTime; + details.durationSecs = s.durationSecs; + details.state = s.cycleState; + TransitionState storage transitionState = LibAppStorage.transitionState(); + details.nextTaskIndexPosition = transitionState.nextTaskIndexPosition; + details.expectedTasksToBeProcessed = LibUtils.uint256ArrayToUint64Array(transitionState.expectedTasksToBeProcessed); + } + + /// @notice Returns if automation is enabled. + function isAutomationEnabled() public view returns (bool) { + return LibAppStorage.appStorage().automationEnabled; + } + + /// @notice Returns true only if the Automation Registry is both fully initialized + /// (see IRegistryStatus.isInitialized) and automation is currently enabled -- a single + /// combined readiness check for callers (e.g. node-runtime) that need both facts before + /// treating the registry as usable. + /// @dev Calls isInitialized() through the diamond (address(this)), not a private copy of + /// that check, so this always reflects whichever facet currently serves that selector -- + /// it can't drift out of sync after a future diamondCut replaces the loupe facet alone. + /// Node's off-chain automation registry manager relies on existence of this function. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function isAutomationReadyEnabled() external view returns (bool) { + return IRegistryStatus(address(this)).isInitialized() && isAutomationEnabled(); + } + + /// @notice Removes registered tasks when predicate validation fails during runtime. + /// @dev The node's off-chain VM-signer decoder hardcodes this function's selector and calls + /// it when predicate validation fails at runtime. Do not Remove this selector via + /// diamondCut post-genesis; Replace (to ship a fix) is fine. + /// @param _taskIndex index of the task that has a fatal error. + /// @param _reason explained reason of task removal. + function removeRegisteredTask(uint64 cycleIndex, uint64 _taskIndex, string memory _reason) external { + msg.sender.enforceIsVmSigner(); + + AppStorage storage s = LibAppStorage.appStorage(); + // Check if automation is enabled and cycle is started, else revert with invalid operation error. + // This will give clear feedback to downstream users on requested action status. + if (!s.automationEnabled || !LibCommon.isCycleStarted()) { revert InvalidOperationForCurrentCycleState(); } + // If cycle index doesn't match, revert. + if (s.index != cycleIndex) { revert InvalidInputCycleIndex(); } + + uint64 cycleEndTime = LibCommon.getCycleEndTime(); + uint64 currentTime = uint64(block.timestamp); + // Calculate refundable fee for this remaining time task in current cycle + uint64 residualInterval = cycleEndTime <= currentTime ? 0 : (cycleEndTime - currentTime); + + LibCommon.RemovedTask memory rt = LibCore.handleTasksRemoval(_taskIndex, cycleEndTime, currentTime, residualInterval, _reason); + emit TaskRemovedBySystem(rt); + } + + + function getSelectors() external pure override returns (bytes4[] memory selectors) { + selectors = new bytes4[](11); + selectors[0] = CoreFacet.processTasks.selector; + selectors[1] = CoreFacet.monitorCycleEnd.selector; + selectors[2] = CoreFacet.enableAutomation.selector; + selectors[3] = CoreFacet.disableAutomation.selector; + selectors[4] = CoreFacet.removeRegisteredTask.selector; + selectors[5] = CoreFacet.getCycleInfo.selector; + selectors[6] = CoreFacet.getCycleDuration.selector; + selectors[7] = CoreFacet.getTransitionInfo.selector; + selectors[8] = this.isAutomationEnabled.selector; + selectors[9] = CoreFacet.getCycleStateDetails.selector; + selectors[10] = CoreFacet.isAutomationReadyEnabled.selector; + } +} diff --git a/solidity/supra_contracts/src/facets/DiamondCutFacet.sol b/solidity/supra_contracts/src/facets/DiamondCutFacet.sol new file mode 100644 index 0000000000..06d4d0e11b --- /dev/null +++ b/solidity/supra_contracts/src/facets/DiamondCutFacet.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; + +// Remember to add the loupe functions from DiamondLoupeFacet to the diamond. +// The loupe functions are required by the EIP2535 Diamonds standard + +contract DiamondCutFacet is IDiamondCut { + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata + ) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.diamondCut(_diamondCut, _init, _calldata); + } +} diff --git a/solidity/supra_contracts/src/facets/DiamondLoupeFacet.sol b/solidity/supra_contracts/src/facets/DiamondLoupeFacet.sol new file mode 100644 index 0000000000..7a81249053 --- /dev/null +++ b/solidity/supra_contracts/src/facets/DiamondLoupeFacet.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { IDiamondLoupe } from "../interfaces/IDiamondLoupe.sol"; +import { IERC165 } from "../interfaces/IERC165.sol"; +import { IFacetSelectors } from "../interfaces/IFacetSelectors.sol"; +import { IRegistryStatus } from "../interfaces/IRegistryStatus.sol"; + +// The functions in DiamondLoupeFacet MUST be added to a diamond. +// The EIP-2535 Diamond standard requires these functions. + +contract DiamondLoupeFacet is IDiamondLoupe, IERC165, IFacetSelectors, IRegistryStatus { + + // Diamond Loupe Functions + //////////////////////////////////////////////////////////////////// + /// These functions are expected to be called frequently by tools. + // + // struct Facet { + // address facetAddress; + // bytes4[] functionSelectors; + // } + + /// @notice Gets all facets and their selectors. + /// @return facets_ Facet + function facets() external override view returns (Facet[] memory facets_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + uint256 numFacets = ds.facetAddresses.length; + facets_ = new Facet[](numFacets); + for (uint256 i; i < numFacets; i++) { + address facetAddress_ = ds.facetAddresses[i]; + facets_[i].facetAddress = facetAddress_; + facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors; + } + } + + /// @notice Gets all the function selectors provided by a facet. + /// @param _facet The facet address. + /// @return facetFunctionSelectors_ + function facetFunctionSelectors(address _facet) external override view returns (bytes4[] memory facetFunctionSelectors_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors; + } + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external override view returns (address[] memory facetAddresses_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetAddresses_ = ds.facetAddresses; + } + + /// @notice Gets the facet that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external override view returns (address facetAddress_) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress; + } + + // This implements ERC-165. + function supportsInterface(bytes4 _interfaceId) external override view returns (bool) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + return ds.supportedInterfaces[_interfaceId]; + } + + /// @notice Returns true if the Automation Registry has completed its DiamondInit initialization. + /// @dev Reads LibDiamond.DiamondStorage.initialized directly -- a dedicated flag, not the + /// ERC-165 `supportedInterfaces` registry. Those two are unrelated concerns: ERC-165 answers + /// "which standards does this diamond implement", not "has genesis initialization + /// completed", and coupling this check to specific interface registrations would mean any + /// future change to ERC-165 registration has to also reason about whether it disturbs this + /// signal. `initialized` is set exactly once, at the end of DiamondInit.init(), which is + /// itself `initializer`-guarded (OpenZeppelin Initializable) so it can never run a second + /// time -- so this can only ever go false -> true, once, for the life of a given Diamond. + /// Node's off-chain automation registry manager relies on existence of it. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function isInitialized() external override view returns (bool) { + return LibDiamond.diamondStorage().initialized; + } + + function getSelectors() external pure override returns (bytes4[] memory s) { + s = new bytes4[](6); + s[0] = DiamondLoupeFacet.facets.selector; + s[1] = DiamondLoupeFacet.facetFunctionSelectors.selector; + s[2] = DiamondLoupeFacet.facetAddresses.selector; + s[3] = DiamondLoupeFacet.facetAddress.selector; + s[4] = DiamondLoupeFacet.supportsInterface.selector; + s[5] = DiamondLoupeFacet.isInitialized.selector; + } +} diff --git a/solidity/supra_contracts/src/facets/OwnershipFacet.sol b/solidity/supra_contracts/src/facets/OwnershipFacet.sol new file mode 100644 index 0000000000..b05ed2358b --- /dev/null +++ b/solidity/supra_contracts/src/facets/OwnershipFacet.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { IERC173 } from "../interfaces/IERC173.sol"; +import { IFacetSelectors } from "../interfaces/IFacetSelectors.sol"; + +contract OwnershipFacet is IERC173, IFacetSelectors { + + function transferOwnership(address _newOwner) external override { + LibDiamond.enforceIsContractOwner(); + LibDiamond.setContractOwner(_newOwner); + } + + function owner() external override view returns (address owner_) { + owner_ = LibDiamond.contractOwner(); + } + + function getSelectors() external pure override returns (bytes4[] memory s) { + s = new bytes4[](2); + s[0] = OwnershipFacet.owner.selector; + s[1] = OwnershipFacet.transferOwnership.selector; + } +} diff --git a/solidity/supra_contracts/src/facets/RegistryFacet.sol b/solidity/supra_contracts/src/facets/RegistryFacet.sol new file mode 100644 index 0000000000..aec5edb10e --- /dev/null +++ b/solidity/supra_contracts/src/facets/RegistryFacet.sol @@ -0,0 +1,489 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {AppStorage, LibAppStorage, RegistryState, TaskMetadata} from "../libraries/LibAppStorage.sol"; +import {LibAccounting} from "../libraries/LibAccounting.sol"; +import {LibCommon} from "../libraries/LibCommon.sol"; +import {LibRegistry} from "../libraries/LibRegistry.sol"; +import {IRegistryFacet} from "../interfaces/IRegistryFacet.sol"; +import {IFacetSelectors} from "../interfaces/IFacetSelectors.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract RegistryFacet is IRegistryFacet, IFacetSelectors { + using EnumerableSet for *; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: TASKS RELATED FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function used to register a user task for automation. + /// @param _payloadTx Includes the target smart contract address and the data to call in abi encoded form. + /// @param _predicate Payload for predicate of the task. + /// @param _expiryTime Time after which the task gets expired. + /// @param _maxGasAmount Maximum amount of gas for the automation task. + /// @param _gasPriceCap Maximum gas willing to pay for the task. + /// @param _automationFeeCapForCycle Maximum automation fee for a cycle to be paid ever. + /// @param _priority Priority for the task. 0 for default priority. + /// @param _auxData Auxiliary data to be passed. + function register( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint64 _priority, + bytes[] memory _auxData + ) external { + uint64 taskIndex = LibRegistry.registerTask( + _payloadTx, + _predicate, + _expiryTime, + _maxGasAmount, + _gasPriceCap, + _automationFeeCapForCycle, + _priority, + LibCommon.TaskType.UST, + _auxData + ); + + RegistryState storage registryState = LibAppStorage.registryState(); + + registryState.totalDepositedAutomationFees += _automationFeeCapForCycle; + + uint128 flatRegistrationFee = LibAppStorage.activeConfig().flatRegistrationFeeWei; + uint128 fee = flatRegistrationFee + _automationFeeCapForCycle; + + bool sent = IERC20(LibAppStorage.appStorage().erc20Supra).transferFrom(msg.sender, address(this), fee); + if (!sent) { revert TransferFailed(); } + + emit TaskRegistered(taskIndex, msg.sender, flatRegistrationFee, _automationFeeCapForCycle, registryState.tasks[taskIndex]); + } + + /// @notice Function to register a system task. Reverts if caller is not authorized. + /// @param _payloadTx Includes the target smart contract address and the data to call in abi encoded form. + /// @param _predicate Payload for predicate of the task. + /// @param _expiryTime Time after which the task gets expired. + /// @param _maxGasAmount Maximum amount of gas for the automation task. + /// @param _priority Priority for the task. 0 for default priority. + /// @param _auxData Auxiliary data to be passed. + function registerSystemTask( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _priority, + bytes[] memory _auxData + ) external { + if (!isAuthorizedSubmitter(msg.sender)) { revert UnauthorizedAccount(); } + + uint64 taskIndex = LibRegistry.registerTask( + _payloadTx, + _predicate, + _expiryTime, + _maxGasAmount, + 0, + 0, + _priority, + LibCommon.TaskType.GST, + _auxData + ); + + emit SystemTaskRegistered(taskIndex, msg.sender, block.timestamp, LibAppStorage.registryState().tasks[taskIndex]); + } + + /// @notice Cancels the automation tasks with specified task indexes. + /// Only existing task, which is PENDING or ACTIVE, can be cancelled and only by task owner. + /// If the task is + /// - active, its state is updated to be CANCELLED. + /// - pending, it is removed form the list. + /// - cancelled, an error is reported + /// Committed gas limit is updated by reducing it with the max gas amount of the cancelled task. + /// @param _taskIndexes Array of task indexes to be cancelled. + function cancelTasks( + uint64[] memory _taskIndexes + ) external { + validateInput(_taskIndexes); + + LibCommon.TaskCancelled[] memory cancelledTasksBuffer = new LibCommon.TaskCancelled[](_taskIndexes.length); + uint256 counter; + + for (uint256 i; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + cancelledTasksBuffer[counter++] = LibRegistry.cancelTask(taskId, false); + } + } + + if (counter > 0) { + // Emit only the entries actually written. + LibCommon.TaskCancelled[] memory cancelledTasks = new LibCommon.TaskCancelled[](counter); + for (uint256 i; i < counter; i++) { + cancelledTasks[i] = cancelledTasksBuffer[i]; + } + emit TasksCancelled(cancelledTasks, msg.sender); + } + } + + /// @notice Cancels the system automation tasks with specified task indexes. + /// Only existing task, which is PENDING or ACTIVE, can be cancelled and only by task owner. + /// If the task is + /// - active, its state is updated to be CANCELLED. + /// - pending, it is removed form the list. + /// - cancelled, an error is reported + /// Committed gas limit is updated by reducing it with the max gas amount of the cancelled task. + /// @param _taskIndexes Array of task indexes to be cancelled. + function cancelSystemTasks( + uint64[] memory _taskIndexes + ) external { + validateInput(_taskIndexes); + + LibCommon.TaskCancelled[] memory cancelledTasksBuffer = new LibCommon.TaskCancelled[](_taskIndexes.length); + uint256 counter; + + for (uint256 i; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + cancelledTasksBuffer[counter++] = LibRegistry.cancelTask(taskId, true); + } + } + + if (counter > 0) { + // Emit only the entries actually written. + LibCommon.TaskCancelled[] memory cancelledTasks = new LibCommon.TaskCancelled[](counter); + for (uint256 i; i < counter; i++) { + cancelledTasks[i] = cancelledTasksBuffer[i]; + } + emit TasksCancelled(cancelledTasks, msg.sender); + } + } + + /// @notice Immediately stops automation tasks for the specified `_taskIndexes`. + /// Only tasks that exist and are owned by the sender can be stopped. + /// If any of the specified tasks are not owned by the sender, the transaction will abort. + /// When a task is stopped, the committed gas for the next cycle is reduced + /// by the max gas amount of the stopped task. Half of the remaining task fee is refunded. + /// @param _taskIndexes Array of task indexes to be stopped. + function stopTasks( + uint64[] memory _taskIndexes + ) external { + validateInput(_taskIndexes); + + LibCommon.TaskStopped[] memory stoppedTasksBuffer = new LibCommon.TaskStopped[](_taskIndexes.length); + uint64 cycleEndTime = LibCommon.getCycleEndTime(); + uint64 currentTime = uint64(block.timestamp); + // Calculate refundable fee for this remaining time task in current cycle + uint64 residualInterval = cycleEndTime <= currentTime ? 0 : (cycleEndTime - currentTime); + + uint256 counter; + uint128 totalRefundFee; + + // Loop through each task index to validate and stop the task + for (uint256 i = 0; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + (LibCommon.TaskStopped memory ts, uint128 refund) = LibRegistry.stopTask( + taskId, + cycleEndTime, + currentTime, + residualInterval, + false + ); + stoppedTasksBuffer[counter++] = ts; + totalRefundFee += refund; + } + } + + // Refund and emit event if any tasks were stopped + if (counter > 0) { + LibAccounting.refund(msg.sender, totalRefundFee); + + // Emit only the entries actually written. + LibCommon.TaskStopped[] memory stoppedTasks = new LibCommon.TaskStopped[](counter); + for (uint256 i; i < counter; i++) { + stoppedTasks[i] = stoppedTasksBuffer[i]; + } + + // Emit task stopped event + emit TasksStopped(stoppedTasks, msg.sender); + } + } + + /// @notice Immediately stops system automation tasks for the specified `_taskIndexes`. + /// Only tasks that exist and are owned by the sender can be stopped. + /// If any of the specified tasks are not owned by the sender, the transaction will abort. + /// When a task is stopped, the committed gas for the next cycle is reduced + /// by the max gas amount of the stopped task. + /// @param _taskIndexes Array of task indexes to be stopped. + function stopSystemTasks( + uint64[] memory _taskIndexes + ) external { + validateInput(_taskIndexes); + + LibCommon.TaskStopped[] memory stoppedTasksBuffer = new LibCommon.TaskStopped[](_taskIndexes.length); + uint64 cycleEndTime = LibCommon.getCycleEndTime(); + uint64 currentTime = uint64(block.timestamp); + uint256 counter; + + // Loop through each task index to validate and stop the task + for (uint256 i = 0; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + (LibCommon.TaskStopped memory ts,) = LibRegistry.stopTask(taskId, cycleEndTime, currentTime, 0, true); + stoppedTasksBuffer[counter++] = ts; + } + } + + if (counter > 0) { + // Emit only the entries actually written. + LibCommon.TaskStopped[] memory stoppedTasks = new LibCommon.TaskStopped[](counter); + for (uint256 i; i < counter; i++) { + stoppedTasks[i] = stoppedTasksBuffer[i]; + } + + // Emit task stopped event + emit TasksStopped(stoppedTasks, msg.sender); + } + } + + /// @notice Helper function for validation. + function validateInput(uint64[] memory _taskIndexes) private view { + if (!LibAppStorage.appStorage().automationEnabled) { revert AutomationNotEnabled(); } + if (!LibCommon.isCycleStarted()) revert CycleTransitionInProgress(); + if (_taskIndexes.length == 0) revert TaskIndexesCannotBeEmpty(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns all the automation tasks available in the registry. + /// @dev Node's off-chain automation registry manager relies on existence of it. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function getTaskIdList() external view returns (uint256[] memory) { + return LibAppStorage.registryState().taskIdList.values(); + } + + /// @notice Returns all the automation tasks registered by an address. + /// @param _addr Address to fetch registered tasks for. + function getTasksByAddress(address _addr) external view returns (uint256[] memory) { + return LibAppStorage.registryState().addressToTasks[_addr].values(); + } + + /// @notice Returns all the system tasks available in the registry. + function getSystemTaskIds() external view returns (uint256[] memory) { + return LibAppStorage.registryState().sysTaskIds.values(); + } + + /// @notice Returns the owner of the task + /// @param _taskIndex Task index of the task to query. + function getTaskOwner(uint64 _taskIndex) external view returns (address) { + return LibAppStorage.registryState().tasks[_taskIndex].owner; + } + + /// @notice Returns the next task index. + function getNextTaskIndex() external view returns (uint64) { + return LibAppStorage.registryState().currentIndex; + } + + /// @notice Returns the number of total tasks. + function totalTasks() external view returns (uint256) { + return LibAppStorage.registryState().taskIdList.length(); + } + + /// @notice Returns the number of total system tasks. + function totalSystemTasks() external view returns (uint256) { + return LibAppStorage.registryState().sysTaskIds.length(); + } + + /// @notice Returns if a task exists in the registry. + /// @param _taskIndex Task index to check existence for. + function ifTaskExists(uint64 _taskIndex) external view returns (bool) { + return LibCommon.ifTaskExists(_taskIndex); + } + + /// @notice Returns if a system task exists in the registry. + /// @param _taskIndex Task index of the system task to check existence for. + function ifSysTaskExists(uint64 _taskIndex) external view returns (bool) { + return LibAppStorage.registryState().sysTaskIds.contains(_taskIndex); + } + + /// @notice Returns the details of a task. Reverts if task doesn't exist. + /// @dev Node's off-chain automation registry manager relies on existence of it. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function getTaskDetails(uint64 _taskIndex) external view returns (TaskMetadata memory) { + return LibCommon.getTask(_taskIndex); + } + + /// @notice Retrieves the details of automation tasks by their task index. Skips a task if it doesn't exist. + /// @param _taskIndexes Input task indexes to get details of. + /// @return Task details of the tasks that exist. + function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (TaskMetadata[] memory) { + uint256 count = _taskIndexes.length; + TaskMetadata[] memory temp = new TaskMetadata[](count); + uint256 exists; + + for (uint256 i = 0; i < count; i++) { + if (LibCommon.ifTaskExists(_taskIndexes[i])) { + temp[exists] = LibAppStorage.registryState().tasks[_taskIndexes[i]]; + exists += 1; + } + } + + TaskMetadata[] memory taskDetails = new TaskMetadata[](exists); + for (uint256 i = 0; i < exists; i++) { + taskDetails[i] = temp[i]; + } + return taskDetails; + } + + /// @notice Checks if the input account is an authorized submitter to submit system automation tasks. + /// @param _account Address to check if it's authorized. + function isAuthorizedSubmitter(address _account) public view returns (bool) { + return LibAppStorage.appStorage().authorizedAccounts.contains(_account); + } + + /// @notice Returns the total number of active tasks. + function getTotalActiveTasks() external view returns (uint256) { + return LibAppStorage.registryState().activeTaskIds.length; + } + + /// @notice Returns all the active task indexes. + /// @dev Node's off-chain automation registry manager relies on existence of it. + /// Update/Replace is acceptable, but removal should be checked against node-runtime first. + function getActiveTaskIds() external view returns (uint256[] memory) { + return LibAppStorage.registryState().activeTaskIds; + } + + /// @notice Checks whether there is an active task in registry with specified input task index. + function hasActiveUserTask(address _account, uint64 _taskIndex) external view returns (bool) { + return hasActiveTaskOfType(_account, _taskIndex, LibCommon.TaskType.UST); + } + + /// @notice Checks whether there is an active system task in registry with specified input task index. + function hasActiveSystemTask(address _account, uint64 _taskIndex) external view returns (bool) { + return hasActiveTaskOfType(_account, _taskIndex, LibCommon.TaskType.GST); + } + + /// @notice Checks whether there is an active task in registry with specified input task index of the input type. + /// The type can be either 0 for user submitted tasks, and 1 for governance authorized tasks. + function hasActiveTaskOfType(address _account, uint64 _taskIndex, LibCommon.TaskType _type) public view returns (bool) { + TaskMetadata storage task = LibAppStorage.registryState().tasks[_taskIndex]; + return task.owner == _account && task.taskState != LibCommon.TaskState.PENDING && task.taskType == _type; + } + + /// @notice Returns the gas committed for the next cycle. + function getGasCommittedForNextCycle() external view returns (uint128) { + return LibAppStorage.registryState().gasCommittedForNextCycle; + } + + /// @notice Returns the gas committed for the current cycle. + function getGasCommittedForCurrentCycle() external view returns (uint128) { + return LibAppStorage.registryState().gasCommittedForThisCycle; + } + + /// @notice Returns the system gas committed for the next cycle. + function getSystemGasCommittedForNextCycle() external view returns (uint128) { + return LibAppStorage.registryState().sysGasCommittedForNextCycle; + } + + /// @notice Returns the system gas committed for the current cycle. + function getSystemGasCommittedForCurrentCycle() external view returns (uint128) { + return LibAppStorage.registryState().sysGasCommittedForThisCycle; + } + + /// @notice Returns the registry max gas cap for the next cycle. + function getNextCycleRegistryMaxGasCap() external view returns (uint128) { + return LibAppStorage.registryState().nextCycleRegistryMaxGasCap; + } + + /// @notice Returns the system registry max gas cap for the next cycle. + function getNextCycleSysRegistryMaxGasCap() external view returns (uint128) { + return LibAppStorage.registryState().nextCycleSysRegistryMaxGasCap; + } + + /// @notice Returns the locked fees for the cycle. + function getCycleLockedFees() external view returns (uint256) { + return LibAppStorage.registryState().cycleLockedFees; + } + + /// @notice Returns the total amount of automation fees deposited. + function getTotalDepositedAutomationFees() external view returns (uint256) { + return LibAppStorage.registryState().totalDepositedAutomationFees; + } + + /// @notice Returns the total amount locked which comprises of 'cycleLockedFees' and 'totalDepositedAutomationFees'. + function getTotalLockedBalance() external view returns (uint256) { + RegistryState storage registryState = LibAppStorage.registryState(); + return registryState.cycleLockedFees + registryState.totalDepositedAutomationFees; + } + + /// @notice Calculates automation fee per second for the specified task occupancy + /// referencing the current automation registry fee parameters, specified total/committed occupancy and current registry + /// maximum allowed occupancy. + function calculateAutomationFeeMultiplierForCommittedOccupancy(uint128 _totalCommittedMaxGas) external view returns (uint128) { + return LibAccounting.calculateAutomationFeeMultiplierForCommittedOccupancy(_totalCommittedMaxGas); + } + + /// @notice Calculates the automation fee multiplier for current cycle. + function calculateAutomationFeeMultiplierForCurrentCycle() external view returns (uint128) { + return LibAccounting.calculateAutomationFeeMultiplierForCurrentCycle(); + } + + /// @notice Estimates automation fee for the next cycle for specified task occupancy for the configured cycle-interval + /// referencing the current automation registry fee parameters, current total occupancy and registry maximum allowed + /// occupancy for the next cycle. + function estimateAutomationFee(uint128 _taskOccupancy) external view returns (uint128) { + return LibAccounting.estimateAutomationFeeWithCommittedOccupancyInternal(_taskOccupancy, LibAppStorage.registryState().gasCommittedForNextCycle); + } + + /// @notice Estimates automation fee the next cycle for specified task occupancy for the configured cycle-interval + /// referencing the current automation registry fee parameters, specified total/committed occupancy and registry + /// maximum allowed occupancy for the next cycle. + function estimateAutomationFeeWithCommittedOccupancy( + uint128 _taskOccupancy, + uint128 _committedOccupancy + ) external view returns (uint128) { + return LibAccounting.estimateAutomationFeeWithCommittedOccupancyInternal( + _taskOccupancy, + _committedOccupancy + ); + } + + function getSelectors() external pure override returns (bytes4[] memory selectors) { + selectors = new bytes4[](36); + selectors[0] = RegistryFacet.register.selector; + selectors[1] = RegistryFacet.registerSystemTask.selector; + selectors[2] = RegistryFacet.cancelTasks.selector; + selectors[3] = RegistryFacet.cancelSystemTasks.selector; + selectors[4] = RegistryFacet.stopTasks.selector; + selectors[5] = RegistryFacet.stopSystemTasks.selector; + selectors[6] = RegistryFacet.getTaskIdList.selector; + selectors[7] = RegistryFacet.getSystemTaskIds.selector; + selectors[8] = RegistryFacet.getTaskOwner.selector; + selectors[9] = RegistryFacet.getNextTaskIndex.selector; + selectors[10] = RegistryFacet.totalTasks.selector; + selectors[11] = RegistryFacet.totalSystemTasks.selector; + selectors[12] = RegistryFacet.getTaskDetails.selector; + selectors[13] = RegistryFacet.getTaskDetailsBulk.selector; + selectors[14] = this.isAuthorizedSubmitter.selector; + selectors[15] = RegistryFacet.getTotalActiveTasks.selector; + selectors[16] = RegistryFacet.getActiveTaskIds.selector; + selectors[17] = this.hasActiveUserTask.selector; + selectors[18] = this.hasActiveSystemTask.selector; + selectors[19] = this.hasActiveTaskOfType.selector; + selectors[20] = RegistryFacet.getGasCommittedForNextCycle.selector; + selectors[21] = RegistryFacet.getGasCommittedForCurrentCycle.selector; + selectors[22] = RegistryFacet.getSystemGasCommittedForNextCycle.selector; + selectors[23] = RegistryFacet.getSystemGasCommittedForCurrentCycle.selector; + selectors[24] = RegistryFacet.getNextCycleRegistryMaxGasCap.selector; + selectors[25] = RegistryFacet.getNextCycleSysRegistryMaxGasCap.selector; + selectors[26] = RegistryFacet.getCycleLockedFees.selector; + selectors[27] = RegistryFacet.getTotalDepositedAutomationFees.selector; + selectors[28] = RegistryFacet.getTotalLockedBalance.selector; + selectors[29] = RegistryFacet.calculateAutomationFeeMultiplierForCommittedOccupancy.selector; + selectors[30] = RegistryFacet.calculateAutomationFeeMultiplierForCurrentCycle.selector; + selectors[31] = RegistryFacet.estimateAutomationFee.selector; + selectors[32] = RegistryFacet.estimateAutomationFeeWithCommittedOccupancy.selector; + selectors[33] = RegistryFacet.ifTaskExists.selector; + selectors[34] = RegistryFacet.ifSysTaskExists.selector; + selectors[35] = RegistryFacet.getTasksByAddress.selector; + } +} diff --git a/solidity/supra_contracts/src/interfaces/IBlockMeta.sol b/solidity/supra_contracts/src/interfaces/IBlockMeta.sol new file mode 100644 index 0000000000..9cd5a6e240 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IBlockMeta.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IBlockMeta { + /// @notice Thrown when the caller is not the VM signer. + error CallerNotVmSigner(); + /// @notice Thrown when an out-of-bounds index is supplied. + error InvalidIndex(); + /// @notice Thrown when a zero or otherwise invalid selector is supplied. + error InvalidSelector(); + /// @notice Thrown when a (target, selector) pair is already registered. + error SelectorAlreadyRegistered(); + /// @notice Thrown when a (target, selector) pair is not found in the execution list. + error SelectorNotRegistered(); + /// @notice Thrown when a zero gas limit is supplied. + error InvalidGasLimit(); + /// @notice Thrown when the gas cap is zero or current total allocated gas is greater than 63/64 of the gas cap. + error InvalidGasCap(); + /// @notice Thrown when the total allocated gas exceeds the block prologue gas cap taking into account 63/64 forwarding rule. + error GasCapExceeded(); + /// @notice Thrown when an empty execution array is provided to 'updateExecutionOrder'. + error InvalidExecutionsLength(); + + /// @notice Emitted when a function selector is registered for per-block execution. + /// @param targetContract Address of the target contract. + /// @param selector Function selector to be called on the target contract. + /// @param gasLimit Gas limit allocated for this function. + event SelectorRegistered(address indexed targetContract, bytes4 indexed selector, uint64 indexed gasLimit); + + /// @notice Emitted when a function selector is removed from per-block execution. + /// @param targetContract Address of the target contract. + /// @param selector Deregistered function selector. + /// @param gasLimit Gas limit that was allocated for this function. + event SelectorDeregistered(address indexed targetContract, bytes4 indexed selector, uint64 gasLimit); + + /// @notice Emitted when the full execution order is replaced. + /// @param executionOrder Updated array of packed execution entries. + event ExecutionOrderUpdated(uint256[] indexed executionOrder); + + /// @notice Emitted when a per-block call to a registered function fails. + /// @param targetContract Address of the target contract. + /// @param selector Called function selector. + /// @param returndata Data returned by the failed call. + event CallFailed(address indexed targetContract, bytes4 indexed selector, bytes returndata); + + /// @notice Emitted when a per-block call to a registered function succeeds. + /// @param targetContract Address of the target contract. + /// @param selector Called function selector. + event CallSucceeded(address indexed targetContract, bytes4 indexed selector); + + /// @notice Emitted when the block prologue gas cap is updated. + /// @param cap The new gas cap for the entire block prologue. + event BlockPrologueGasCapUpdated(uint64 indexed cap); + + /// @notice Registers a (target, selector) pair for per-block execution. + /// @param _targetContract The target contract address. + /// @param _selector Function selector to call on the target contract. + /// @param _gasLimit Gas limit for this function. + function register(address _targetContract, bytes4 _selector, uint64 _gasLimit) external; + + /// @notice Deregisters a (target, selector) pair by value. + /// @param _targetContract The target contract address. + /// @param _selector The function selector to deregister. + function deregister(address _targetContract, bytes4 _selector) external; + + /// @notice Deregisters the entry at a given index in the execution order. + /// @param _index Index in the executions array. + function deregisterAt(uint256 _index) external; + + /// @notice Replaces the entire execution order with a new list of packed entries. + /// @dev Each entry must be packed as [target(160) | selector(32) | gasLimit(64)]. + /// @param _executions Array of packed execution entries (target|selector|gasLimit) representing the new order. + function updateExecutionOrder(uint256[] calldata _executions) external; + + /// @notice Sets the total gas cap for the block prologue. + /// @param _cap The new total gas cap. + function setBlockPrologueGasCap(uint64 _cap) external; + + /// @notice Returns all registered (target, selector) pairs in execution order. + /// @return targets Array of target contract addresses. + /// @return selectors Array of function selectors corresponding to each target. + function getExecutions() external view returns (address[] memory targets, bytes4[] memory selectors); + + /// @notice Returns the unique set of registered target contract addresses. + /// @return targetContracts Deduplicated array of registered target addresses. + function getTargetContracts() external view returns (address[] memory targetContracts); + + /// @notice Returns all selectors registered for a given target contract. + /// @param _targetContract The target contract address. + /// @return selectors Array of function selectors registered for the target. + function getSelectors(address _targetContract) external view returns (bytes4[] memory selectors); + + /// @notice Returns the (target, selector) pair at a given execution index. + /// @param _index Position in the execution order array. + /// @return target The target contract address. + /// @return selector The function selector to be called on the target. + function getExecutionAt(uint256 _index) external view returns (address target, bytes4 selector); + + /// @notice Returns the execution index for a given (target, selector) pair. + /// @param _targetContract The target contract address. + /// @param _selector The function selector registered for the target. + /// @return index The index in the execution order array. + function getExecutionIndex(address _targetContract, bytes4 _selector) external view returns (uint256 index); + + /// @notice Returns the gas limit for a given (target, selector) pair. + /// @param _targetContract The target contract address. + /// @param _selector The function selector. + /// @return gasLimit Gas limit allocated for calls to this function. + function getExecutionGasLimit(address _targetContract, bytes4 _selector) external view returns (uint64 gasLimit); + + /// @notice Returns the total gas cap for the block prologue. + /// @return cap The total gas cap. + function blockPrologueGasCap() external view returns (uint64 cap); + + /// @notice Returns the sum of all per-entry gas limits currently registered. + /// @return totalGas Total allocated gas. + function totalGasAllocated() external view returns (uint64 totalGas); +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/interfaces/IConfigFacet.sol b/solidity/supra_contracts/src/interfaces/IConfigFacet.sol new file mode 100644 index 0000000000..35dc1e3cea --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IConfigFacet.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Config} from "../libraries/LibAppStorage.sol"; + +interface IConfigFacet { + // ============================================================= + // Events + // ============================================================= + /// @notice Emitted when an account is authorized as submitter for system tasks. + event AuthorizationGranted(address indexed account, uint256 indexed timestamp); + + /// @notice Emitted when authorization is revoked for an account to submit system tasks. + event AuthorizationRevoked(address indexed account, uint256 indexed timestamp); + + /// @notice Emitted when task registration is enabled. + event TaskRegistrationEnabled(bool indexed status); + + /// @notice Emitted when task registration is disabled. + event TaskRegistrationDisabled(bool indexed status); + + /// @notice Emitted when the registry fees is withdrawn by the admin. + event RegistryFeeWithdrawn(address indexed recipient, uint256 indexed feesWithdrawn); + + /// @notice Emitted when a new config is added. + event ConfigBufferUpdated(Config indexed pendingConfig); + + /// @notice Emitted when the task registration input size caps are updated. + event DataLengthCapsUpdated(uint16 maxPayloadLength, uint16 maxPredicateLength, uint16 maxAuxDataLength, uint16 maxAuxDataEntries); + + + // ============================================================= + // Custom errors + // ============================================================= + error AddressAlreadyExists(); + error AddressDoesNotExist(); + error AlreadyDisabled(); + error AlreadyEnabled(); + error InvalidAmount(); + error InsufficientBalance(); + error RequestExceedsLockedBalance(); + error TransferFailed(); + error UnacceptableRegistryMaxGasCap(); + error UnacceptableSysRegistryMaxGasCap(); + + // ============================================================= + // View functions + // ============================================================= + function erc20Supra() external view returns (address); + function getConfig() external view returns (Config memory); + function getConfigBuffer() external view returns (Config memory); + function isRegistrationEnabled() external view returns (bool); + function getDataLengthCaps() external view returns (uint16 maxPayloadLength, uint16 maxPredicateLength, uint16 maxAuxDataLength, uint16 maxAuxDataEntries); + + // ============================================================= + // State update functions + // ============================================================= + function grantAuthorization(address _account) external; + function revokeAuthorization(address _account) external; + function enableRegistration() external; + function disableRegistration() external; + function withdrawFees(uint256 _amount, address _recipient) external; + function updateConfigBuffer( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint8 _congestionThresholdPercentage, + uint128 _congestionBaseFeeWeiPerSec, + uint8 _congestionExponent, + uint8 _maxCongestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) external; + function updateDataLengthCaps( + uint16 _maxPayloadLength, + uint16 _maxPredicateLength, + uint16 _maxAuxDataLength, + uint16 _maxAuxDataEntries + ) external; +} diff --git a/solidity/supra_contracts/src/interfaces/ICoreFacet.sol b/solidity/supra_contracts/src/interfaces/ICoreFacet.sol new file mode 100644 index 0000000000..581884275e --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/ICoreFacet.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibCommon} from "../libraries/LibCommon.sol"; + +interface ICoreFacet { + // ============================================================= + // Events + // ============================================================= + /// @notice Emitted when automation is enabled. + event AutomationEnabled(bool indexed status); + + /// @notice Emitted when automation is disabled. + event AutomationDisabled(bool indexed status); + + /// @notice Event emitted on cycle transition containing active task indexes for the new cycle. + event ActiveTasks(uint256[] indexed taskIndexes); + + /// @notice Event emitted on cycle transition containing removed task indexes. + event RemovedTasks(uint64[] indexed taskIndexes); + + /// @notice Emitted when the cycle state transitions. + event AutomationCycleEvent( + uint64 indexed index, + LibCommon.CycleState indexed state, + uint64 startTime, + uint64 durationSecs, + LibCommon.CycleState indexed oldState + ); + + /// @notice Emitted when an automation fee is charged for an automation task for the cycle. + event TaskCycleFeeWithdraw( + uint64 cycleIndex, + uint64 indexed taskIndex, + address indexed owner, + uint128 indexed fee + ); + + /// @notice Emitted when a task is removed as fee exceeds task's automation fee cap for the cycle. + event TaskCancelledCapacitySurpassed( + uint64 indexed taskIndex, + address owner, + uint128 indexed fee, + uint128 indexed automationFeeCapForCycle, + bytes32 registrationHash + ); + + /// @notice Emitted when a task is removed due to insufficient balance or allowance. + event TaskCancelledInsufficentBalanceAllowance( + uint64 indexed taskIndex, + address owner, + uint128 indexed fee, + uint256 indexed balance, + uint256 allowance, + bytes32 registrationHash + ); + + /// @notice Emitted when tasks are removed by system due to various reasons. + event TaskRemovedBySystem(LibCommon.RemovedTask indexed removedTask); + + // ============================================================= + // Custom errors + // ============================================================= + error AlreadyDisabled(); + error AlreadyEnabled(); + error InconsistentTransitionState(); + error InsufficientBalanceForRefund(); + error InvalidArrayLength(); + error InvalidInputCycleIndex(); + error InvalidOperationForCurrentCycleState(); + error InvalidRegistryState(); + error OutOfOrderTaskProcessingRequest(); + error RegisteredTaskInvalidType(); + error TaskIndexNotFound(); + error TransferFailed(); + error UnknownTaskToProcess(uint64 taskIndex); + + // ============================================================= + // View functions + // ============================================================= + function getCycleInfo() external view returns (uint64, uint64, uint64, LibCommon.CycleState); + function getCycleDuration() external view returns (uint64); + function getTransitionInfo() external view returns (uint64, uint128); + function isAutomationEnabled() external view returns (bool); + function isAutomationReadyEnabled() external view returns (bool); + function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory); + + // ============================================================= + // State update functions + // ============================================================= + function monitorCycleEnd() external; + function processTasks(uint64 _cycleIndex, uint256[] memory _taskIndexes) external; + function enableAutomation() external; + function disableAutomation() external; + function removeRegisteredTask(uint64 _cycleIndex, uint64 _taskIndex, string memory _reason) external; +} diff --git a/solidity/supra_contracts/src/interfaces/IDiamondCut.sol b/solidity/supra_contracts/src/interfaces/IDiamondCut.sol new file mode 100644 index 0000000000..8681a2cc08 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IDiamondCut.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +interface IDiamondCut { + enum FacetCutAction {Add, Replace, Remove} + // Add=0, Replace=1, Remove=2 + + struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; + } + + /// @notice Add/replace/remove any number of functions and optionally execute + /// a function with delegatecall + /// @param _diamondCut Contains the facet addresses and function selectors + /// @param _init The address of the contract or facet to execute _calldata + /// @param _calldata A function call, including function selector and arguments + /// _calldata is executed with delegatecall on _init + function diamondCut( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata + ) external; + + event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata); +} diff --git a/solidity/supra_contracts/src/interfaces/IDiamondLoupe.sol b/solidity/supra_contracts/src/interfaces/IDiamondLoupe.sol new file mode 100644 index 0000000000..20bc6acd30 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IDiamondLoupe.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ + +// A loupe is a small magnifying glass used to look at diamonds. +// These functions look at diamonds +interface IDiamondLoupe { + /// These functions are expected to be called frequently + /// by tools. + + struct Facet { + address facetAddress; + bytes4[] functionSelectors; + } + + /// @notice Gets all facet addresses and their four byte function selectors. + /// @return facets_ Facet + function facets() external view returns (Facet[] memory facets_); + + /// @notice Gets all the function selectors supported by a specific facet. + /// @param _facet The facet address. + /// @return facetFunctionSelectors_ + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_); + + /// @notice Get all the facet addresses used by a diamond. + /// @return facetAddresses_ + function facetAddresses() external view returns (address[] memory facetAddresses_); + + /// @notice Gets the facet that supports the given selector. + /// @dev If facet is not found return address(0). + /// @param _functionSelector The function selector. + /// @return facetAddress_ The facet address. + function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_); +} diff --git a/solidity/supra_contracts/src/interfaces/IERC165.sol b/solidity/supra_contracts/src/interfaces/IERC165.sol new file mode 100644 index 0000000000..8a8a7c6128 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/solidity/supra_contracts/src/interfaces/IERC173.sol b/solidity/supra_contracts/src/interfaces/IERC173.sol new file mode 100644 index 0000000000..4cb8ca9e20 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC173.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/// @title ERC-173 Contract Ownership Standard +/// Note: the ERC-165 identifier for this interface is 0x7f5828d0 +/* is ERC165 */ +interface IERC173 { + /// @dev This emits when ownership of a contract changes. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Get the address of the owner + /// @return owner_ The address of the owner. + function owner() external view returns (address owner_); + + /// @notice Set the address of the new owner of the contract + /// @dev Reverts with LibDiamond.AddressCannotBeZero if `_newOwner` is the zero address. + /// Renouncing ownership is intentionally not supported. + /// @param _newOwner The address of the new owner of the contract + function transferOwnership(address _newOwner) external; +} diff --git a/solidity/supra_contracts/src/interfaces/IERC20Supra.sol b/solidity/supra_contracts/src/interfaces/IERC20Supra.sol new file mode 100644 index 0000000000..bad6e2e38e --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC20Supra.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IERC20Supra is IERC20 { + /// @notice Thrown when a function is called by an address that is not authorized to perform the operation. + error UnauthorizedCaller(); + /// @notice Thrown when trying to add an already authorized address. + error AddressAlreadyAuthorized(); + /// @notice Thrown when trying to remove an address that is not authorized. + error AddressNotAuthorized(); + + /// @notice Emitted when the contract is initialized with authorized addresses. + /// @param authorizedAddresses The list of authorized addresses. + event InitializedAuthorizedAddresses(address[] indexed authorizedAddresses); + + /// @notice Emitted when an address is added to the authorization whitelist. + /// @param authorizedAddress The address that was added. + /// @param addedBy The address that added the authorized address. + event AuthorizedAddressAdded(address indexed authorizedAddress, address indexed addedBy); + + /// @notice Emitted when an address is removed from the authorization whitelist. + /// @param authorizedAddress The address that was removed. + /// @param removedBy The address that removed the authorized address. + event AuthorizedAddressRemoved(address indexed authorizedAddress, address indexed removedBy); + + function mint(address _to, uint256 _amount) external; + function burn(uint256 _amount) external; + function burnFrom(address _from, uint256 _amount) external; + function addAuthorizedAddress(address _addr) external; + function removeAuthorizedAddress(address _addr) external; +} diff --git a/solidity/supra_contracts/src/interfaces/IERC20SupraHandler.sol b/solidity/supra_contracts/src/interfaces/IERC20SupraHandler.sol new file mode 100644 index 0000000000..1e7b1ce7e2 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC20SupraHandler.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IERC20SupraHandler { + /// @notice Thrown when a user has insufficient ERC20Supra balance to withdraw. + error InsufficientBalance(); + /// @notice Thrown when the contract holds insufficient native balance to fulfil a withdrawal. + error InsufficientContractBalance(); + /// @notice Thrown when zero is passed as an amount. + error InvalidAmount(); + /// @notice Thrown when the low-level native-token transfer fails. + error TransferFailed(); + + /// @notice Emitted when native tokens are deposited and ERC20Supra tokens are minted 1:1. + /// @param account Address of the depositor. + /// @param amount Amount of native tokens deposited. + event Deposit(address indexed account, uint256 indexed amount); + + /// @notice Emitted when ERC20Supra tokens are burned and native tokens are returned 1:1. + /// @param account Address of the withdrawer. + /// @param amount Amount of native tokens withdrawn. + event Withdrawal(address indexed account, uint256 indexed amount); + + /// @notice Returns the address of the ERC20Supra contract. + function erc20Supra() external view returns (address); + + /// @notice Deposits native tokens and mints an equal amount of ERC20Supra tokens to the caller. + function deposit() external payable; + + /// @notice Burns ERC20Supra tokens and returns an equal amount of native tokens to the caller. + /// @param _amount Amount of tokens to withdraw. + function withdraw(uint256 _amount) external; +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/interfaces/IFacetSelectors.sol b/solidity/supra_contracts/src/interfaces/IFacetSelectors.sol new file mode 100644 index 0000000000..3999f2057e --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IFacetSelectors.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/// @notice Implemented by every facet so the Diamond constructor can +/// retrieve its function selectors without a central registry. +interface IFacetSelectors { + function getSelectors() external pure returns (bytes4[] memory); +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/interfaces/IMultiSignatureWallet.sol b/solidity/supra_contracts/src/interfaces/IMultiSignatureWallet.sol new file mode 100644 index 0000000000..e11d28dd0d --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IMultiSignatureWallet.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IMultiSignatureWallet { + // ── Errors ──────────────────────────────────────────────────────────────── + + /// @notice Thrown when the caller is not a registered owner. + error NotAnOwner(); + /// @notice Thrown when a transaction index references a non-existent transaction. + error InvalidTxnId(); + /// @notice Thrown when attempting to act on an already-executed transaction. + error TxnAlreadyExecuted(); + /// @notice Thrown when an owner tries to confirm a transaction they already confirmed. + error TxnAlreadyConfirmed(); + /// @notice Thrown when the owners array supplied during initialization is empty. + error OwnersRequired(); + /// @notice Thrown when the required confirmation count is zero or exceeds the owner count. + error InvalidNumberOfConfirmations(); + /// @notice Thrown when a zero address is supplied as an owner. + error InvalidOwner(); + /// @notice Thrown when address(0) is supplied as a transaction recipient. + error InvalidRecipient(); + /// @notice Thrown when a duplicate owner address is provided. + error OwnerNotUnique(); + /// @notice Thrown when a transaction does not have enough confirmations to execute. + error NotEnoughConfirmation(); + /// @notice Thrown when the low-level transaction call fails. + error ExecutionFailed(); + /// @notice Thrown when empty creation bytecode is passed to deployContract. + error EmptyCreationCode(); + /// @notice Thrown when a CREATE deployment returns address(0). + error ContractCreationFailed(); + /// @notice Thrown when revoking a confirmation the caller has not given. + error TransactionNotConfirmed(); + /// @notice Thrown when an admin function is called by anyone other than the multisig itself. + error OnlyMultisigAccountCanCall(); + /// @notice Thrown when submitTransaction is given a timeout duration longer than maxTimeoutDuration. + error TimeoutTooLong(); + /// @notice Thrown when confirming, executing, or revoking a transaction whose timeout has already passed. + error TransactionAlreadyExpired(); + /// @notice Thrown when removeExpiredTransaction is called on a transaction that has not actually expired. + error NotExpired(); + /// @notice Thrown when updateMaxTimeoutDuration is called with a zero duration. + error InvalidMaxTimeoutDuration(); + + // ── Events ──────────────────────────────────────────────────────────────── + + /// @notice Emitted when native tokens are received by the wallet. + /// @param sender Address that sent the funds. + /// @param amount Amount deposited. + /// @param balance New contract balance after the deposit. + event Deposit(address indexed sender, uint256 amount, uint256 balance); + + /// @notice Emitted when a new transaction is submitted. + /// @param owner Owner who submitted the transaction. + /// @param txIndex Index assigned to the transaction. + /// @param to Target contract address. + /// @param value ETH value attached to the transaction. + /// @param data Call data payload. + event SubmitTransaction( + address indexed owner, + uint256 indexed txIndex, + address indexed to, + uint256 value, + bytes data + ); + + /// @notice Emitted when a pending transaction expires and is removed. + /// @param txIndex Index of the expired transaction. + event TransactionExpired(uint256 indexed txIndex); + + /// @notice Emitted when an owner confirms a transaction. + /// @param owner Owner who confirmed. + /// @param txIndex Index of the confirmed transaction. + event ConfirmTransaction(address indexed owner, uint256 indexed txIndex); + + /// @notice Emitted when an owner revokes a previously given confirmation. + /// @param owner Owner who revoked. + /// @param txIndex Index of the transaction. + event RevokeConfirmation(address indexed owner, uint256 indexed txIndex); + + /// @notice Emitted when a transaction is executed. + /// @param owner Owner who triggered execution. + /// @param txIndex Index of the executed transaction. + /// @param txData Data returned by the executed call. + event ExecuteTransaction(address indexed owner, uint256 indexed txIndex, bytes txData); + + /// @notice Emitted when a new contract is deployed via deployContract. + /// @param deployedContract Address of the newly deployed contract. + event ContractDeployed(address indexed deployedContract); + + /// @notice Emitted when new owners are added to the wallet. + /// @param owners Array of newly added owner addresses. + event OwnersAdded(address[] owners); + + /// @notice Emitted when owners are removed from the wallet. + /// @param owners Array of removed owner addresses. + event OwnersRemoved(address[] owners); + + /// @notice Emitted when the required confirmation count is updated. + /// @param newNumConfirmation The new required confirmation count. + event NumConfirmationUpdated(uint256 newNumConfirmation); + + /// @notice Emitted when a pending transaction is cancelled by the multisig. + /// @param txIndex Index of the cancelled transaction. + event TransactionCancelled(uint256 indexed txIndex); + + /// @notice Emitted when the maximum allowed submission timeout duration is updated. + /// @param newMaxTimeoutDuration The new maximum timeout duration, in seconds. + event MaxTimeoutDurationUpdated(uint64 newMaxTimeoutDuration); + + // ── State variable getters ──────────────────────────────────────────────── + + /// @notice Returns the number of confirmations required to execute a transaction. + function numConfirmationsRequired() external view returns (uint256); + + /// @notice Returns the current number of pending (non-executed) transactions. + function txCount() external view returns (uint256); + + /// @notice Returns the maximum timeout duration, in seconds, allowed for a newly submitted transaction. + function maxTimeoutDuration() external view returns (uint64); + + // ── Core wallet functions ───────────────────────────────────────────────── + + /// @notice Submits a new transaction for confirmation by other owners. + /// @param _to Target contract address. + /// @param _value Amount of ETH to send with the transaction. + /// @param _timeoutDuration Seconds from now after which the transaction expires. + /// @param _data Call data payload. + function submitTransaction( + address _to, + uint256 _value, + uint64 _timeoutDuration, + bytes memory _data + ) external payable; + + /// @notice Confirms a pending transaction. Reverts if it has expired; use + /// removeExpiredTransaction to clean up an expired one. + /// @param _txIndex Index of the transaction to confirm. + function confirmTransaction(uint256 _txIndex) external; + + /// @notice Executes a transaction once enough confirmations are gathered. Reverts if it has + /// expired; use removeExpiredTransaction to clean up an expired one. + /// @param _txIndex Index of the transaction to execute. + /// @return Data returned by the executed call. + function executeTransaction(uint256 _txIndex) external returns (bytes memory); + + /// @notice Revokes a previously given confirmation. Reverts if the transaction has expired; + /// use removeExpiredTransaction to clean up an expired one. + /// @param _txIndex Index of the transaction. + function revokeConfirmation(uint256 _txIndex) external; + + /// @notice Removes an already-expired transaction from storage. Callable by anyone. + /// @param _txIndex Index of the expired transaction to remove. + function removeExpiredTransaction(uint256 _txIndex) external; + + // ── Admin functions (callable only by the multisig itself) ──────────────── + + /// @notice Adds new owners to the wallet. + /// @param _owners Array of addresses to add as owners. + function addOwners(address[] memory _owners) external; + + /// @notice Removes existing owners from the wallet. + /// @param _owners Array of owner addresses to remove. + function removeOwners(address[] memory _owners) external; + + /// @notice Updates the required confirmation count. + /// @param _numConfirmationsRequired New confirmation threshold. + function updateNumConfirmations(uint256 _numConfirmationsRequired) external; + + /// @notice Cancels a pending transaction, removing it from storage. + /// @param _txIndex Index of the transaction to cancel. + function cancelTransaction(uint256 _txIndex) external; + + /// @notice Updates the maximum timeout duration allowed for newly submitted transactions. + /// @param _newMaxTimeoutDuration New maximum timeout duration, in seconds. + function updateMaxTimeoutDuration(uint64 _newMaxTimeoutDuration) external; + + /// @notice Deploys a contract using the CREATE opcode. + /// @param _creationCode Creation bytecode of the contract to deploy. + /// @param _value Amount of ETH to forward with deployment. + /// @return deployed Address of the newly deployed contract. + function deployContract(bytes memory _creationCode, uint256 _value) external returns (address deployed); + + // ── View functions ──────────────────────────────────────────────────────── + + /// @notice Returns the list of current owner addresses. + function getOwners() external view returns (address[] memory); + + /// @notice Returns the index that will be assigned to the next submitted transaction. + function getNextTransactionIndex() external view returns (uint256); + + /// @notice Returns whether a given owner has confirmed a transaction. + /// @param _txIndex Index of the transaction. + /// @param _owner Address of the owner to check. + function isConfirmed(uint256 _txIndex, address _owner) external view returns (bool); + + /// @notice Returns the details of a pending transaction. + /// @param _txIndex Index of the transaction. + /// @return to Target contract address. + /// @return value ETH value attached to the transaction. + /// @return numConfirmations Number of confirmations received so far. + /// @return timeout Expiry timestamp of the transaction. + /// @return data Call data payload. + function getTransaction(uint256 _txIndex) external view returns ( + address to, + uint256 value, + uint24 numConfirmations, + uint64 timeout, + bytes memory data + ); + + /// @notice Function to check if a transaction has a valid number of confirmations. + /// @param _txIndex Index of the transaction to check for. + /// @return bool True if the transaction has a valid number of confirmations counting only valid owners, false otherwise. + function hasValidNumberOfConfirmations(uint256 _txIndex) external view returns (bool); +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/interfaces/IRegistryFacet.sol b/solidity/supra_contracts/src/interfaces/IRegistryFacet.sol new file mode 100644 index 0000000000..a882ee8c2a --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IRegistryFacet.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibCommon} from "../libraries/LibCommon.sol"; +import {TaskMetadata} from "../libraries/LibAppStorage.sol"; + +interface IRegistryFacet { + // ============================================================= + // Events + // ============================================================= + /// @notice Emitted when a user task is registered. + event TaskRegistered( + uint64 indexed taskIndex, + address indexed owner, + uint128 registrationFee, + uint128 lockedDepositFee, + TaskMetadata indexed taskMetadata + ); + + /// @notice Emitted when a system task is registered. + event SystemTaskRegistered( + uint64 indexed taskIndex, + address indexed owner, + uint256 timestamp, + TaskMetadata taskMetadata + ); + + /// @notice Emitted when a task is cancelled. + event TasksCancelled( + LibCommon.TaskCancelled[] indexed cancelledTasks, + address indexed owner + ); + + /// @notice Emitted when a task is stopped. + event TasksStopped( + LibCommon.TaskStopped[] indexed stoppedTasks, + address indexed owner + ); + + /// @notice Emitted when an automation fee is refunded for an automation task at the end of the cycle for excessive + /// duration paid at the beginning of the cycle due to cycle duration reduction by governance. + event TaskFeeRefund( + uint64 indexed taskIndex, + address indexed owner, + uint128 indexed amount + ); + + /// @notice Emitted when a deposit fee is refunded for an automation task. + event TaskDepositFeeRefund(uint64 indexed taskIndex, address indexed owner, uint128 indexed amount); + + /// @notice Emitted when a task cycle fee is being refunded but locked cycle fees is less than the requested refund. + event ErrorUnlockTaskCycleFee( + uint64 indexed taskIndex, + uint256 indexed lockedCycleFees, + uint128 indexed refund + ); + + /// @notice Emitted during cycle transition when refunds to be paid is not possible due to insufficient contract balance. + /// Type of the refund can be related either to the deposit paid during registration (0), or to cycle fee caused by + /// the shortening of the cycle (1) + event ErrorInsufficientBalanceToRefund( + uint64 indexed _taskIndex, + address indexed _owner, + uint8 indexed _refundType, + uint128 _amount + ); + + /// @notice Emitted when deposit fee is being refunded but total locked deposits is less than the locked deposit for the task. + event ErrorUnlockTaskDepositFee( + uint64 indexed taskIndex, + uint256 indexed totalDepositedAutomationFees, + uint128 indexed lockedDeposit + ); + + + // ============================================================= + // Custom errors + // ============================================================= + error AlreadyCancelled(); + error AutomationNotEnabled(); + error CycleTransitionInProgress(); + error ErrorCycleFeeRefund(); + error ErrorDepositRefund(); + error FailedToCallTxHashPrecompile(); + error GasCommittedExceedsMaxGasCap(); + error GasCommittedValueUnderflow(); + error InsufficientFeeCapForCycle(uint128 estimatedAutomationFeeForCycle); + error InvalidCycleRefundFee(); + error InvalidExpiryTime(); + error InvalidGasPriceCap(); + error InvalidMaxGasAmount(); + error InvalidPayloadLength(); + error PayloadTooLarge(); + error PredicateTooLarge(); + error AuxDataTooLarge(); + error InvalidReturnLengthOfPredicate(); + error InvalidReturnTypeOfPredicate(); + error InvalidRegistryState(); + error InvalidTaskDuration(); + error RegistrationDisabled(); + error StaticCallToPredicateFailed(); + error TaskCapacityReached(); + error TaskExpiresBeforeNextCycle(); + error TaskIndexNotFound(); + error TaskIndexNotUnique(); + error TaskIndexesCannotBeEmpty(); + error TransferFailed(); + error TxnHashLengthShouldBe32(uint64); + error UnauthorizedAccount(); + error UnsupportedTaskOperation(); + + // ============================================================= + // View functions + // ============================================================= + function calculateAutomationFeeMultiplierForCommittedOccupancy(uint128 _totalCommittedMaxGas) external view returns (uint128); + function calculateAutomationFeeMultiplierForCurrentCycle() external view returns (uint128); + function estimateAutomationFee(uint128 _taskOccupancy) external view returns (uint128); + function estimateAutomationFeeWithCommittedOccupancy(uint128 _taskOccupancy, uint128 _committedOccupancy) external view returns (uint128); + function isAuthorizedSubmitter(address _account) external view returns (bool); + function ifTaskExists(uint64 _taskIndex) external view returns (bool); + function ifSysTaskExists(uint64 _taskIndex) external view returns (bool); + function getActiveTaskIds() external view returns (uint256[] memory); + function getCycleLockedFees() external view returns (uint256); + function getGasCommittedForCurrentCycle() external view returns (uint128); + function getGasCommittedForNextCycle() external view returns (uint128); + function getNextCycleRegistryMaxGasCap() external view returns (uint128); + function getNextCycleSysRegistryMaxGasCap() external view returns (uint128); + function getNextTaskIndex() external view returns (uint64); + function getSystemGasCommittedForCurrentCycle() external view returns (uint128); + function getSystemGasCommittedForNextCycle() external view returns (uint128); + function getSystemTaskIds() external view returns (uint256[] memory); + function getTaskDetails(uint64 _taskIndex) external view returns (TaskMetadata memory); + function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (TaskMetadata[] memory); + function getTaskIdList() external view returns (uint256[] memory); + function getTaskOwner(uint64 _taskIndex) external view returns (address); + function getTotalActiveTasks() external view returns (uint256); + function getTotalDepositedAutomationFees() external view returns (uint256); + function getTotalLockedBalance() external view returns (uint256); + function getTasksByAddress(address _addr) external view returns (uint256[] memory); + function hasActiveSystemTask(address _account, uint64 _taskIndex) external view returns (bool); + function hasActiveTaskOfType(address _account, uint64 _taskIndex, LibCommon.TaskType _type) external view returns (bool); + function hasActiveUserTask(address _account, uint64 _taskIndex) external view returns (bool); + function totalSystemTasks() external view returns (uint256); + function totalTasks() external view returns (uint256); + + // ============================================================= + // State update functions + // ============================================================= + function register( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint64 _priority, + bytes[] memory _auxData + ) external; + function registerSystemTask( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _priority, + bytes[] memory _auxData + ) external; + function cancelTasks(uint64[] memory _taskIndexes) external; + function cancelSystemTasks(uint64[] memory _taskIndexes) external; + function stopTasks(uint64[] memory _taskIndexes) external; + function stopSystemTasks(uint64[] memory _taskIndexes) external; +} diff --git a/solidity/supra_contracts/src/interfaces/IRegistryStatus.sol b/solidity/supra_contracts/src/interfaces/IRegistryStatus.sol new file mode 100644 index 0000000000..e648e1c40f --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IRegistryStatus.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/// @title Automation Registry status +/// @notice Kept separate from IDiamondLoupe so adding it does not change +/// type(IDiamondLoupe).interfaceId away from the well-known EIP-2535 value. +interface IRegistryStatus { + /// @notice Returns true if the Automation Registry has completed its DiamondInit initialization. + function isInitialized() external view returns (bool); +} diff --git a/solidity/supra_contracts/src/libraries/DiamondTypes.sol b/solidity/supra_contracts/src/libraries/DiamondTypes.sol new file mode 100644 index 0000000000..5d9fe403e6 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/DiamondTypes.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +struct FacetsDeployment { + address diamondCutFacet; + address loupeFacet; + address ownershipFacet; + address configFacet; + address registryFacet; + address coreFacet; + address diamondInit; +} + +struct InitParams { + uint64 taskDurationCapSecs; + uint128 registryMaxGasCap; + uint128 automationBaseFeeWeiPerSec; + uint128 flatRegistrationFeeWei; + uint8 congestionThresholdPercentage; + uint128 congestionBaseFeeWeiPerSec; + uint8 congestionExponent; + uint8 maxCongestionExponent; + uint16 taskCapacity; + uint64 cycleDurationSecs; + uint64 sysTaskDurationCapSecs; + uint128 sysRegistryMaxGasCap; + uint16 sysTaskCapacity; + bool automationEnabled; + bool registrationEnabled; +} diff --git a/solidity/supra_contracts/src/libraries/LibAccounting.sol b/solidity/supra_contracts/src/libraries/LibAccounting.sol new file mode 100644 index 0000000000..6dd5bb78a3 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibAccounting.sol @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {AppStorage, Config, LibAppStorage, RegistryState, TaskMetadataLW} from "./LibAppStorage.sol"; +import {LibCommon} from "./LibCommon.sol"; +import {ICoreFacet} from "../interfaces/ICoreFacet.sol"; +import {IRegistryFacet} from "../interfaces/IRegistryFacet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +library LibAccounting { + + /// @dev Constant for 10^8 + uint256 constant DECIMAL = 100_000_000; + + /// @dev Constants describing REFUND TYPE + uint8 constant DEPOSIT_CYCLE_FEE = 0; + uint8 constant CYCLE_FEE = 1; + + /// @dev Defines divisor for refunds of deposit fees with penalty + /// Factor of `2` suggests that `1/2` of the deposit will be refunded. + uint8 constant REFUND_FACTOR = 2; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: PRIVATE FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Refunds fee paid by the task for the cycle to the task owner. + /// Note that here we do not unlock the fee, as on cycle change locked cycle-fees for the ended cycle are + /// automatically unlocked. + function safeFeeRefund( + uint64 _taskIndex, + address _taskOwner, + uint256 _cycleLockedFees, + uint128 _refundableFee + ) private returns (bool, uint256) { + bool result; + uint256 remainingLockedFees; + + (result, remainingLockedFees) = safeUnlockLockedCycleFee(_cycleLockedFees, _refundableFee, _taskIndex); + if (!result) { return (result, remainingLockedFees); } + + result = safeRefund( _taskIndex, _taskOwner, _refundableFee, CYCLE_FEE); + if (result) { emit IRegistryFacet.TaskFeeRefund(_taskIndex, _taskOwner, _refundableFee); } + return (result, remainingLockedFees); + } + + /// @notice Refunds the specified amount to the task owner. + /// @dev Error event is emitted if the registry contract does not have sufficient balance. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableAmount Amount to refund. + /// @param _refundType Type of refund. + /// @return Bool representing if refund was successful. + function safeRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableAmount, + uint8 _refundType + ) private returns (bool) { + AppStorage storage s = LibAppStorage.appStorage(); + + address erc20Supra = s.erc20Supra; + uint256 balance = IERC20(erc20Supra).balanceOf(address(this)); + if (balance < _refundableAmount) { + emit IRegistryFacet.ErrorInsufficientBalanceToRefund(_taskIndex, _taskOwner, _refundType, _refundableAmount); + return false; + } else { + return _refund(erc20Supra, _taskOwner, _refundableAmount); + } + } + + /// @notice Helper function to transfer refunds. + /// @param _erc20Supra Address of the ERC20Supra token. + /// @param _to Recipeint of the refund + /// @param _amount Amount to refund + /// @return Bool representing if refund was successful. + function _refund(address _erc20Supra, address _to, uint128 _amount) private returns (bool) { + bool sent = IERC20(_erc20Supra).transfer(_to, _amount); + if (!sent) { revert IRegistryFacet.TransferFailed(); } + + return sent; + } + + /// @notice Calculates the automation fee multiplier for cycle. + /// @param _totalCommittedGas Total committed gas. + /// @param _registryMaxGasCap Registry max gas cap. + /// @param _automationBaseFeeWeiPerSec Automation base fee in wei per sec. + function calculateAutomationFeeMultiplierForCycle( + uint128 _totalCommittedGas, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec + ) private view returns (uint128) { + uint128 congesionFee = calculateAutomationCongestionFee(_totalCommittedGas, _registryMaxGasCap); + return (congesionFee + _automationBaseFeeWeiPerSec); + } + + /// @notice Function to calculate the automation congestion fee. + /// @param _totalCommittedGas Total committed gas. + /// @param _registryMaxGasCap Registry max gas cap. + /// @return Returns the automation congestion fee. + function calculateAutomationCongestionFee( + uint128 _totalCommittedGas, + uint128 _registryMaxGasCap + ) private view returns (uint128) { + Config storage activeConfig = LibAppStorage.activeConfig(); + + uint8 congestionThresholdPercentage = activeConfig.congestionThresholdPercentage; + uint8 congestionExponent = activeConfig.congestionExponent; + uint128 congestionBaseFeeWeiPerSec = activeConfig.congestionBaseFeeWeiPerSec; + + if (congestionThresholdPercentage == 100 || congestionBaseFeeWeiPerSec == 0) { return 0; } + + // thresholdUsage = (totalCommittedGas / maxGasCap) * 100 + uint256 thresholdUsageScaled = (uint256(_totalCommittedGas) * DECIMAL * 100) / uint256(_registryMaxGasCap); + + uint256 thresholdPercentageScaled = uint256(congestionThresholdPercentage) * DECIMAL; + + // If usage is below threshold → no congestion fee + if (thresholdUsageScaled <= thresholdPercentageScaled) { + return 0; + } + + // Calculate how much usage exceeds threshold + uint256 surplus; + if (thresholdUsageScaled > 100 * DECIMAL) { + surplus = (100 * DECIMAL) - thresholdPercentageScaled; + } else { + surplus = thresholdUsageScaled - thresholdPercentageScaled; + } + uint256 surplusScaled = surplus / 100; + + uint256 exponentResult = calculateExponentiation( + surplusScaled, + congestionExponent + ); + + // Multiply base fee (wei/sec) with exponentResult and downscale by DECIMAL + uint256 acf = (uint256(congestionBaseFeeWeiPerSec) * exponentResult) / DECIMAL; + + return uint128(acf); + } + + /// @notice Computes exponentiation using fixed-point arithmetic. + function calculateExponentiation( + uint256 _base, + uint8 _exponent + ) private pure returns (uint256) { + uint256 baseScaled = DECIMAL + _base; // (1 + base) + uint256 resultScaled = DECIMAL; + + while (_exponent > 0) { + if ((_exponent & 1) != 0) { + resultScaled = (resultScaled * baseScaled) / DECIMAL; + } + + _exponent >>= 1; + // Only square if another bit remains to consume — the value produced by + // squaring after the last bit is never used, so skip that computation. + if (_exponent > 0) { + baseScaled = (baseScaled * baseScaled) / DECIMAL; + } + } + + return resultScaled - DECIMAL; // subtract 1 + } + + /// @notice Unlocks the locked fee paid by the task for cycle. + /// Error event is emitted if the cycle locked fee amount is inconsistent with the requested unlock amount. + /// @param _cycleLockedFees Locked cycle fees + /// @param _refundableFee Refundable fees + /// @param _taskIndex Index of the task + /// @return Bool if _refundableFee can be unlocked safely. + /// @return Updated _cycleLockedFees after unlocking _refundableFee. + function safeUnlockLockedCycleFee( + uint256 _cycleLockedFees, + uint128 _refundableFee, + uint64 _taskIndex + ) private returns (bool, uint256) { + // This check makes sure that more than locked amount of the fees will be not be refunded. + // Any attempt means internal bug. + bool hasLockedFee = _cycleLockedFees >= _refundableFee; + if (hasLockedFee) { + // Unlock the refunded amount + _cycleLockedFees = _cycleLockedFees - _refundableFee; + } else { + emit IRegistryFacet.ErrorUnlockTaskCycleFee(_taskIndex, _cycleLockedFees, _refundableFee); + } + return (hasLockedFee, _cycleLockedFees); + } + + /// @notice Calculates automation task fees for a single task at the time of new cycle. + /// This is supposed to be called only after removing expired task and must not be called for expired task. + /// @dev _automationFeePerSec is expected to be base automation fee + congestion fee if any. + function calculateAutomationFeeForInterval( + uint64 _duration, + uint128 _taskOccupancy, + uint128 _automationFeePerSec, + uint128 _registryMaxGasCap + ) private pure returns (uint128) { + uint256 taskOccupancyRatioByDuration = (uint256(_duration) * uint256(_taskOccupancy) * DECIMAL) / uint256(_registryMaxGasCap); + + uint256 automationFeeForInterval = _automationFeePerSec * taskOccupancyRatioByDuration; + + return uint128(automationFeeForInterval / DECIMAL); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: INTERNAL FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Refunds the deposit fee and any automation fees of the task. + function refundTaskFees( + uint64 _currentTime, + uint64 _refundDuration, + uint128 _automationFeePerSec, + TaskMetadataLW memory _task + ) internal { + RegistryState storage registryState = LibAppStorage.registryState(); + + // Do not attempt fee refund if remaining duration is 0 + if (_task.taskState != LibCommon.TaskState.PENDING && _refundDuration != 0) { + uint128 _refundFee = calculateTaskFee( + _task.taskState, + _task.expiryTime, + _task.maxGasAmount, + _refundDuration, + _currentTime, + _automationFeePerSec + ); + ( , uint256 remainingCycleLockedFees) = safeFeeRefund( + _task.taskIndex, + _task.owner, + registryState.cycleLockedFees, + _refundFee + ); + registryState.cycleLockedFees = remainingCycleLockedFees; + } + + safeDepositRefund( + _task.taskIndex, + _task.owner, + _task.depositFee, + _task.depositFee + ); + } + + /// @notice Refunds the specified amount of deposit to the task owner and unlocks full deposit from the total automation fees deposited. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableDeposit Refundable amount of deposit. + /// @param _lockedDeposit Total locked deposit. + function safeDepositRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) internal returns (bool) { + // Ensures that amount to unlock is not more than the total automation fees deposited. + bool result = safeUnlockLockedDeposit(_taskIndex, _lockedDeposit); + if (!result) { + return result; + } + + result = safeRefund(_taskIndex, _taskOwner, _refundableDeposit, DEPOSIT_CYCLE_FEE); + + if (result) { emit IRegistryFacet.TaskDepositFeeRefund(_taskIndex, _taskOwner, _refundableDeposit); } + return result; + } + + /// @notice Refunds the deposit fee of the task and removes from the registry during cycle transition. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableDeposit Refundable amount of deposit. + /// @param _lockedDeposit Total locked deposit. + function refundDepositAndDrop( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) internal { + // Check if task is UST + if (LibAppStorage.registryState().tasks[_taskIndex].taskType == LibCommon.TaskType.GST) { revert ICoreFacet.RegisteredTaskInvalidType(); } + + // Remove task from the registry state + LibCommon.removeTask(_taskIndex, _taskOwner, false, false); + + // Refund + safeDepositRefund( + _taskIndex, + _taskOwner, + _refundableDeposit, + _lockedDeposit + ); + } + + /// @notice Internally calls _refund, reverts if the balance is insufficient. + function refund(address _to, uint128 _amount) internal { + if (_amount == 0) return; + AppStorage storage s = LibAppStorage.appStorage(); + + address erc20Supra = s.erc20Supra; + uint256 balance = IERC20(erc20Supra).balanceOf(address(this)); + if (balance < _amount) { revert ICoreFacet.InsufficientBalanceForRefund(); } + _refund(erc20Supra, _to, _amount); + } + + /// @notice Calculates the automation fee multiplier for current cycle. + function calculateAutomationFeeMultiplierForCurrentCycle() internal view returns (uint128) { + Config storage activeConfig = LibAppStorage.activeConfig(); + + // Compute the automation fee multiplier for this cycle + return calculateAutomationFeeMultiplierForCycle( + LibAppStorage.registryState().gasCommittedForThisCycle, + activeConfig.registryMaxGasCap, + activeConfig.automationBaseFeeWeiPerSec + ); + } + + /// @notice Calculates automation fee per second for the specified task occupancy + /// referencing the current automation registry fee parameters, specified total/committed occupancy and current registry + /// maximum allowed occupancy. + function calculateAutomationFeeMultiplierForCommittedOccupancy( + uint128 _totalCommittedMaxGas + ) internal view returns (uint128) { + Config storage activeConfig = LibAppStorage.activeConfig(); + + // Compute the automation fee multiplier for cycle + return calculateAutomationFeeMultiplierForCycle( + _totalCommittedMaxGas, + activeConfig.registryMaxGasCap, + activeConfig.automationBaseFeeWeiPerSec + ); + } + + /// @notice Estimates automation fee the next cycle for specified task occupancy for the configured cycle interval + /// referencing the current automation registry fee parameters, specified total/committed occupancy and registry + /// maximum allowed occupancy for the next cycle. + /// Note it is expected that committed_occupancy does not include current task's occupancy. + function estimateAutomationFeeWithCommittedOccupancyInternal( + uint128 _taskOccupancy, + uint128 _committedOccupancy + ) internal view returns (uint128) { + AppStorage storage s = LibAppStorage.appStorage(); + RegistryState storage registryState = LibAppStorage.registryState(); + + uint128 totalCommittedGas = _taskOccupancy + _committedOccupancy; + + uint128 automationFeePerSec = calculateAutomationFeeMultiplierForCycle( + totalCommittedGas, + registryState.nextCycleRegistryMaxGasCap, + LibAppStorage.activeConfig().automationBaseFeeWeiPerSec + ); + + if (automationFeePerSec == 0) return 0; + return calculateAutomationFeeForInterval(s.durationSecs, _taskOccupancy, automationFeePerSec, registryState.nextCycleRegistryMaxGasCap); + } + + /// @notice Helper function to unlock locked deposit and cycle fees when stopTasks is called. Calculates cycle fee and + /// deposit fee that needs to be refunded. Tries to unlock the same from corresponding counters and returns the refund amounts. + /// Function reverts if unlocking fails. Note that this function does not do actual refund, but only unlocking. + function unlockDepositAndCycleFee( + uint64 _taskIndex, + LibCommon.TaskState _taskState, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _residualInterval, + uint64 _currentTime, + uint128 _depositFee + ) internal returns (uint128, uint128) { + AppStorage storage s = LibAppStorage.appStorage(); + RegistryState storage registryState = LibAppStorage.registryState(); + + uint128 cycleLockedFeeForTask; + uint128 cycleFeeRefund; + uint128 depositRefund; + + if (_taskState != LibCommon.TaskState.PENDING) { + // Compute the automation fee multiplier for cycle + Config storage activeConfig = LibAppStorage.activeConfig(); + uint128 automationFeePerSec = calculateAutomationFeeMultiplierForCycle( + registryState.gasCommittedForThisCycle, + activeConfig.registryMaxGasCap, + activeConfig.automationBaseFeeWeiPerSec + ); + uint64 chargedDuration = 0; + if (_expiryTime > s.startTime) { + chargedDuration = uint64(Math.min(_expiryTime - s.startTime, s.durationSecs)); + } + uint128 taskFeeForCurrentCycle = calculateAutomationFeeForInterval(chargedDuration, _maxGasAmount, automationFeePerSec, activeConfig.registryMaxGasCap); + uint128 taskFeeForResidualTime = calculateTaskFee( + _taskState, + _expiryTime, + _maxGasAmount, + _residualInterval, + _currentTime, + automationFeePerSec + ); + + // Refund full deposit and half of the remaining run-time fee when a task is in active or cancelled stage + cycleLockedFeeForTask = taskFeeForCurrentCycle; + cycleFeeRefund = taskFeeForResidualTime / REFUND_FACTOR; + depositRefund = _depositFee; + } else { + cycleLockedFeeForTask = 0; + cycleFeeRefund = 0; + depositRefund = _depositFee / REFUND_FACTOR; + } + + bool result = safeUnlockLockedDeposit(_taskIndex, _depositFee); + if (!result) { revert IRegistryFacet.ErrorDepositRefund(); } + + if (cycleLockedFeeForTask < cycleFeeRefund) { revert IRegistryFacet.InvalidCycleRefundFee(); } + + (bool hasLockedFee, uint256 remainingCycleLockedFees ) = safeUnlockLockedCycleFee(registryState.cycleLockedFees, cycleLockedFeeForTask, _taskIndex); + if (!hasLockedFee) { revert IRegistryFacet.ErrorCycleFeeRefund(); } + + registryState.cycleLockedFees = remainingCycleLockedFees; + + return (cycleFeeRefund, depositRefund); + } + + /// @notice Unlocks the deposit paid by the task from the total automation fees deposited. + /// @dev Error event is emitted if the total automation fees deposited is less than the requested unlock amount. + /// @param _taskIndex Index of the task. + /// @param _lockedDeposit Locked deposit amount to be unlocked. + /// @return Bool if _lockedDeposit can be unlocked safely. + function safeUnlockLockedDeposit( + uint64 _taskIndex, + uint128 _lockedDeposit + ) internal returns (bool) { + RegistryState storage registryState = LibAppStorage.registryState(); + + uint256 totalDeposited = registryState.totalDepositedAutomationFees; + + if (totalDeposited >= _lockedDeposit) { + registryState.totalDepositedAutomationFees = totalDeposited - _lockedDeposit; + return true; + } + + emit IRegistryFacet.ErrorUnlockTaskDepositFee(_taskIndex, totalDeposited, _lockedDeposit); + return false; + } + + /// @notice Calculates automation task fees for a single task at the time of new cycle. + /// This is supposed to be called only after removing expired task and must not be called for expired task. + /// @param _state State of the task. + /// @param _expiryTime Task expiry time. + /// @param _maxGasAmount Task's max gas amount + /// @param _potentialFeeTimeframe Potential time frame to calculate task fees for. + /// @param _currentTime Current time + /// @param _automationFeePerSec Automation fee per sec + /// @return Calculated task fee for the interval the task will be active. + function calculateTaskFee( + LibCommon.TaskState _state, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _potentialFeeTimeframe, + uint64 _currentTime, + uint128 _automationFeePerSec + ) internal view returns (uint128) { + if (_automationFeePerSec == 0) { return 0; } + if (_expiryTime <= _currentTime) { return 0; } + + uint64 taskActiveTimeframe = _expiryTime - _currentTime; + + // If the task is a new task i.e. in Pending state, then it is charged always for + // the input _potentialFeeTimeframe(which is cycle-interval), + // For the new tasks which active-timeframe is less than cycle-interval + // it would mean it is their first and only cycle and we charge the fee for entire cycle. + // Note that although the new short tasks are charged for entire cycle, the refunding logic remains the same for + // them as for the long tasks. + // This way bad-actors will be discourged to submit small and short tasks with big occupancy by blocking other + // good-actors register tasks. + uint64 actualFeeTimeframe; + if (_state == LibCommon.TaskState.PENDING) { + actualFeeTimeframe = _potentialFeeTimeframe; + } else { + actualFeeTimeframe = taskActiveTimeframe < _potentialFeeTimeframe ? taskActiveTimeframe : _potentialFeeTimeframe; + } + + return calculateAutomationFeeForInterval( + actualFeeTimeframe, + _maxGasAmount, + _automationFeePerSec, + LibAppStorage.activeConfig().registryMaxGasCap + ); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/libraries/LibAppStorage.sol b/solidity/supra_contracts/src/libraries/LibAppStorage.sol new file mode 100644 index 0000000000..cfe19b7aa4 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibAppStorage.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibCommon} from "../libraries/LibCommon.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Struct representing Automation Registry configuration. +struct Config { + uint128 registryMaxGasCap; + uint128 sysRegistryMaxGasCap; + uint128 automationBaseFeeWeiPerSec; + uint128 flatRegistrationFeeWei; + uint128 congestionBaseFeeWeiPerSec; + uint64 taskDurationCapSecs; + uint64 sysTaskDurationCapSecs; + uint64 cycleDurationSecs; + uint16 taskCapacity; + uint16 sysTaskCapacity; + uint8 congestionThresholdPercentage; + uint8 congestionExponent; + // Governance-set ceiling on congestionExponent, enforced by LibCommon.validateConfigParameters. + // Raising congestionExponent above the current default (6) requires first raising this cap in + // a separate governance action — deliberate friction on a parameter whose growth is exponential + // (see LibAccounting.calculateExponentiation). + uint8 maxCongestionExponent; +} + +/// @notice Struct representing cycle state transition information. +struct TransitionState { + uint256 lockedFees; + uint128 automationFeePerSec; + uint128 gasCommittedForNewCycle; + uint128 gasCommittedForNextCycle; + uint128 sysGasCommittedForNextCycle; + uint64 refundDuration; + uint64 newCycleDuration; + uint64 nextTaskIndexPosition; + // Plain array rather than EnumerableSet.UintSet: this field is only ever accessed + // sequentially (push/length/index) and never via contains()/remove(), so the + // EnumerableSet's second per-element SSTORE (the _positions membership mapping) + // is pure overhead here — see LibCore.updateExpectedTasks. + uint256[] expectedTasksToBeProcessed; + // Task IDs confirmed to survive this transition (appended incrementally by + // LibCore.dropOrChargeTasks, one batch at a time), consumed directly by + // LibCore.updateRegistryState to seed the new cycle's activeTaskIds without + // re-scanning taskIdList from scratch at finalization. + uint256[] survivedTaskIds; +} + +/// @notice Task metadata for individual automation tasks. +struct TaskMetadata { + uint128 maxGasAmount; + uint128 gasPriceCap; + uint128 automationFeeCapForCycle; + uint128 depositFee; + bytes32 txHash; + uint64 taskIndex; + uint64 registrationTime; + uint64 expiryTime; + uint64 priority; + address owner; + LibCommon.TaskType taskType; + LibCommon.TaskState taskState; + bytes payloadTx; + bytes predicate; + bytes[] auxData; +} + +/// @notice Lightweight projection of TaskMetadata for charging/lifecycle logic that +/// never reads payloadTx/predicate/auxData. Populating this instead of the full +/// TaskMetadata avoids copying those dynamic byte blobs from storage to memory. +struct TaskMetadataLW { + uint128 maxGasAmount; + uint128 automationFeeCapForCycle; + uint128 depositFee; + bytes32 txHash; + uint64 taskIndex; + uint64 expiryTime; + address owner; + LibCommon.TaskType taskType; + LibCommon.TaskState taskState; +} + +/// @notice Tracks per-cycle Automation Registry state and tasks related information. +struct RegistryState { + uint256 cycleLockedFees; + uint256 totalDepositedAutomationFees; + uint128 gasCommittedForNextCycle; + uint128 gasCommittedForThisCycle; + uint128 sysGasCommittedForNextCycle; + uint128 sysGasCommittedForThisCycle; + uint128 nextCycleRegistryMaxGasCap; + uint128 nextCycleSysRegistryMaxGasCap; + + uint64 currentIndex; + // Plain array, not EnumerableSet.UintSet: no .contains() call site needs O(1) + // arbitrary membership on this set anywhere in the codebase (only .add/.clear/ + // .length/.values previously), and it is now populated wholesale from + // TransitionState.survivedTaskIds at cycle finalization — see LibCore.updateRegistryState. + uint256[] activeTaskIds; + EnumerableSet.UintSet taskIdList; + EnumerableSet.UintSet sysTaskIds; + // Append-only mirror of taskIdList's insertion order (task IDs are assigned + // strictly monotonically, so appending keeps this ascending by construction). + // taskIdList's own iteration order is a separate, unrelated concern — see + // LibCore.buildAliveOrderedTaskIds, which compacts this array at cycle end. + uint256[] orderedTaskIds; + mapping(uint64 => TaskMetadata) tasks; + mapping(address => EnumerableSet.UintSet) addressToTasks; +} + +/// @notice Central AppStorage layout for Diamond proxy +struct AppStorage { + + // ============================================================= + // CONFIGURATION + // ============================================================= + + bool automationEnabled; + bool registrationEnabled; + address erc20Supra; + EnumerableSet.AddressSet authorizedAccounts; + mapping(uint256 => Config) configuration; + bool ifBufferExists; + + /// @notice Owner-configurable upper bounds on task registration input sizes. Applied + /// immediately (no buffering) since they only gate new registrations, which are already + /// gated to cycle state STARTED — there's no mid-cycle-fairness reason to defer them the + /// way fee/gas-cap changes are deferred to the next cycle boundary. + uint16 maxPayloadLength; + uint16 maxPredicateLength; + uint16 maxAuxDataLength; // total across all auxData entries combined + uint16 maxAuxDataEntries; // bounds the array itself, independent of the byte total above + + // ============================================================= + // CYCLE MANAGEMENT + // ============================================================= + + /// @notice Current automation cycle and transition data + uint64 index; + uint64 startTime; + uint64 durationSecs; + LibCommon.CycleState cycleState; + bool ifTransitionStateExists; + mapping(uint256 => TransitionState) transitionState; + + // ============================================================= + // REGISTRY STATE + // ============================================================= + + /// @notice Registry and tasks state + mapping(uint256 => RegistryState) registry; +} + +/// @notice AppStorage accessor for Diamond facets +library LibAppStorage { + + uint256 constant ACTIVE_CONFIG = 0; + uint256 constant BUFFER_CONFIG = 1; + uint256 constant TRANSITION_STATE = 0; + uint256 constant REGISTRY_STATE = 0; + + // ERC-7201-style namespaced storage slot, mirroring LibDiamond.DIAMOND_STORAGE_POSITION's + // own convention. Deliberately not slot 0: Solidity's automatic sequential storage layout + // starts every contract's first declared state variable at slot 0, so a plain state + // variable can never be made immune to a future facet's inherited storage (Ownable, + // Pausable, Initializable, ERC20, etc.) landing there too — the only way to make + // AppStorage's location independent of what any facet inherits is to pin it, via + // assembly, to a slot nothing else's automatic layout will ever compute. + // + // Derivation: keccak256(abi.encode(uint256(keccak256("supra.automation.registry.appstorage")) - 1)) + // & ~bytes32(uint256(0xff)) + bytes32 constant APP_STORAGE_POSITION = 0x1a8caa6e7a3f48200daaf7419e3bce8b119c3099bb3b75a9b1ba416f813ed500; + + function appStorage() internal pure returns (AppStorage storage s) { + bytes32 position = APP_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + function activeConfig() internal view returns (Config storage c) { + AppStorage storage s = appStorage(); + c = s.configuration[ACTIVE_CONFIG]; + } + + function bufferConfig() internal view returns (Config storage c) { + AppStorage storage s = appStorage(); + c = s.configuration[BUFFER_CONFIG]; + } + + function transitionState() internal view returns (TransitionState storage ts) { + AppStorage storage s = appStorage(); + ts = s.transitionState[TRANSITION_STATE]; + } + + function registryState() internal view returns (RegistryState storage rs) { + AppStorage storage s = appStorage(); + rs = s.registry[REGISTRY_STATE]; + } +} diff --git a/solidity/supra_contracts/src/libraries/LibCommon.sol b/solidity/supra_contracts/src/libraries/LibCommon.sol new file mode 100644 index 0000000000..09e44128ad --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibCommon.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {AppStorage, LibAppStorage, RegistryState, TaskMetadata, TaskMetadataLW} from "./LibAppStorage.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +library LibCommon { + using EnumerableSet for EnumerableSet.UintSet; + + struct CycleDetails { + uint64 index; + uint64 startTime; + uint64 durationSecs; + LibCommon.CycleState state; + uint64 nextTaskIndexPosition; + uint64[] expectedTasksToBeProcessed; + } + + /// @notice Enum describing state of the cycle. + enum CycleState { + READY, + STARTED, + FINISHED, + SUSPENDED + } + + /// @notice Enum describing state of a task. + enum TaskState { + PENDING, + ACTIVE, + CANCELLED + } + + /// @notice Enum describing task type. + enum TaskType { + UST, + GST + } + + /// @notice Represents intermediate state of the registry on cycle change. + struct IntermediateStateOfCycleChange { + uint256 cycleLockedFees; + uint128 gasCommittedForNextCycle; + uint128 sysGasCommittedForNextCycle; + uint64[] removedTasks; + } + + /// @notice Struct representing transition result. + struct TransitionResult { + uint128 fees; + uint128 gas; + uint128 sysGas; + bool isRemoved; + } + + /// @notice Struct representing a cancelled task. + struct TaskCancelled{ + uint64 taskIndex; + TaskType taskType; + bytes32 txHash; + } + + /// @notice Struct representing a stopped task. + struct TaskStopped { + uint64 taskIndex; + uint128 depositRefund; + uint128 cycleFeeRefund; + bytes32 txHash; + } + + /// @notice Struct representing a removed task due to predicate failure. + struct RemovedTask { + uint64 taskIndex; + TaskType taskType; + address owner; + bytes32 txHash; + string reason; + } + + /// @notice Struct representing an entry in access list. + struct AccessListEntry { + address addr; + bytes32[] storageKeys; + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ERRORS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + error InvalidTaskDuration(); + error InvalidRegistryMaxGasCap(); + error InvalidCongestionThreshold(); + error InvalidCongestionExponent(); + error InvalidMaxCongestionExponent(); + error CongestionExponentExceedsMax(); + error InvalidTaskCapacity(); + error InvalidCycleDuration(); + error InvalidSysTaskDuration(); + error InvalidSysRegistryMaxGasCap(); + error InvalidSysTaskCapacity(); + error TaskDoesNotExist(); + error TaskIndexNotFound(); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: INTERNAL FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function to validate the registry configuration parameters. + function validateConfigParameters( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint8 _congestionThresholdPercentage, + uint8 _congestionExponent, + uint8 _maxCongestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) internal pure { + if (_taskDurationCapSecs <= _cycleDurationSecs) { revert InvalidTaskDuration(); } + if (_registryMaxGasCap == 0) { revert InvalidRegistryMaxGasCap(); } + if (_congestionThresholdPercentage > 100) { revert InvalidCongestionThreshold(); } + if (_congestionExponent == 0) { revert InvalidCongestionExponent(); } + if (_maxCongestionExponent == 0) { revert InvalidMaxCongestionExponent(); } + if (_congestionExponent > _maxCongestionExponent) { revert CongestionExponentExceedsMax(); } + if (_taskCapacity == 0) { revert InvalidTaskCapacity(); } + if (_cycleDurationSecs == 0) { revert InvalidCycleDuration(); } + if (_sysTaskDurationCapSecs <= _cycleDurationSecs) { revert InvalidSysTaskDuration(); } + if (_sysRegistryMaxGasCap == 0) { revert InvalidSysRegistryMaxGasCap(); } + if (_sysTaskCapacity == 0) { revert InvalidSysTaskCapacity(); } + } + + /// @notice Checks whether cycle is in STARTED state. + function isCycleStarted() internal view returns (bool) { + AppStorage storage s = LibAppStorage.appStorage(); + return s.cycleState == LibCommon.CycleState.STARTED; + } + + /// @notice Returns the cycle end time. + function getCycleEndTime() internal view returns (uint64 cycleEndTime) { + AppStorage storage s = LibAppStorage.appStorage(); + cycleEndTime = s.startTime + s.durationSecs; + } + + /// @notice Checks if a task exist. + /// @param _taskIndex Task index to check if a task exists against it. + function ifTaskExists(uint64 _taskIndex) internal view returns (bool) { + RegistryState storage registryState = LibAppStorage.registryState(); + return registryState.tasks[_taskIndex].owner != address(0) && registryState.taskIdList.contains(_taskIndex); + } + + /// @notice Returns the details of a task. Reverts if task doesn't exist. + /// @param _taskIndex Task index to get details for. + function getTask(uint64 _taskIndex) internal view returns (TaskMetadata storage task) { + if (!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + task = LibAppStorage.registryState().tasks[_taskIndex]; + } + + /// @notice Removes `_value` from a plain uint256[] by linear scan + swap-and-pop. + /// @dev activeTaskIds is a plain array (see LibAppStorage.RegistryState), not an + /// EnumerableSet, because nothing reads it via contains() and it's rebuilt + /// wholesale each cycle from TransitionState.survivedTaskIds (see + /// LibCore.updateRegistryState) — so it doesn't need an O(1)-membership index. + /// This makes a single removal O(k) (k = current length) instead of + /// EnumerableSet's O(1); order doesn't matter here (unlike taskIdList), so + /// swap-and-pop is safe. Bounded by taskCapacity+sysTaskCapacity, the + /// governance-owned system-wide task cap (see ConfigFacet.updateConfigBuffer). + /// @return found True if `_value` was present and removed. + function removeFromActiveTaskIds(uint256[] storage _arr, uint256 _value) private returns (bool found) { + uint256 len = _arr.length; + for (uint256 i = 0; i < len; i++) { + if (_arr[i] == _value) { + _arr[i] = _arr[len - 1]; + _arr.pop(); + return true; + } + } + return false; + } + + /// @notice Projects a stored task into its lightweight, accounting-relevant view. + /// @dev Reads only the scalar fields — never touches the payloadTx/predicate/auxData + /// storage slots, unlike a full `TaskMetadata memory` assignment. + function toLW(TaskMetadata storage _task) internal view returns (TaskMetadataLW memory task) { + task = TaskMetadataLW({ + maxGasAmount: _task.maxGasAmount, + automationFeeCapForCycle: _task.automationFeeCapForCycle, + depositFee: _task.depositFee, + txHash: _task.txHash, + taskIndex: _task.taskIndex, + expiryTime: _task.expiryTime, + owner: _task.owner, + taskType: _task.taskType, + taskState: _task.taskState + }); + } + + /// @notice Returns the lightweight details of a task. Reverts if task doesn't exist. + /// @param _taskIndex Task index to get details for. + function getTaskLW(uint64 _taskIndex) internal view returns (TaskMetadataLW memory task) { + if (!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + task = toLW(LibAppStorage.registryState().tasks[_taskIndex]); + } + + /// @notice Function to remove a task from the registry. + /// @param _taskIndex Index of the task to remove. + /// @param _owner Address of the task owner. + /// @param _removeFromSysReg Wheather to remove from system task registry. + /// @param _removeFromActive Wheather to remove from active task list. + function removeTask(uint64 _taskIndex, address _owner, bool _removeFromSysReg, bool _removeFromActive) internal { + RegistryState storage registryState = LibAppStorage.registryState(); + + if (_removeFromSysReg) { + require(registryState.sysTaskIds.remove(_taskIndex), TaskIndexNotFound()); + } + + delete registryState.tasks[_taskIndex]; + require(registryState.taskIdList.remove(_taskIndex), TaskIndexNotFound()); + require(registryState.addressToTasks[_owner].remove(_taskIndex), TaskIndexNotFound()); + + if (_removeFromActive) { + require(removeFromActiveTaskIds(registryState.activeTaskIds, _taskIndex), TaskIndexNotFound()); + } + } +} diff --git a/solidity/supra_contracts/src/libraries/LibCore.sol b/solidity/supra_contracts/src/libraries/LibCore.sol new file mode 100644 index 0000000000..422fcf168f --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibCore.sol @@ -0,0 +1,801 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibAccounting} from "./LibAccounting.sol"; +import {LibCommon} from "./LibCommon.sol"; +import {LibUtils} from "./LibUtils.sol"; +import {LibRegistry} from "./LibRegistry.sol"; +import {AppStorage, LibAppStorage, RegistryState, TaskMetadata, TaskMetadataLW, TransitionState} from "./LibAppStorage.sol"; +import {ICoreFacet} from "../interfaces/ICoreFacet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +library LibCore { + using LibUtils for address; + using EnumerableSet for EnumerableSet.UintSet; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: PRIVATE FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the number of total tasks. + function totalTasks() private view returns (uint256) { + return LibAppStorage.registryState().taskIdList.length(); + } + + /// @notice Reverts unless `arr` is strictly ascending, with no sorting or mutation. + /// @dev This contract does not sort caller-submitted task batches (dropOrChargeTasks, + /// onCycleSuspend) — it is the downstream caller's (the VM_SIGNER-driven + /// processTasks submitter's) responsibility to supply them pre-sorted, matching + /// the ascending order markTaskProcessed already requires positionally against + /// expectedTasksToBeProcessed. An unordered submission is a caller bug, not + /// something this contract silently corrects: it must fail loudly here rather + /// than mask an upstream ordering defect or pay to fix it up itself. + /// @param arr The array to validate. Not modified. + function requireSortedAscending(uint256[] memory arr) private pure { + for (uint256 i = 1; i < arr.length; i++) { + if (arr[i - 1] >= arr[i]) revert ICoreFacet.OutOfOrderTaskProcessingRequest(); + } + } + + /// @notice Builds the ascending list of currently-alive task IDs from + /// RegistryState.orderedTaskIds, compacting out tombstoned (removed) entries. + /// @dev orderedTaskIds is append-only (see LibRegistry.createAndStoreTask) and task IDs + /// are assigned strictly monotonically, so it stays ascending by construction — + /// no sort is ever needed. A removed task's slot in `tasks` is `delete`d by + /// LibCommon.removeTask, which zeroes `owner`; that's reused here as a free + /// tombstone flag. The write-back at the end compacts the array in storage, + /// which is mandatory: without it, tombstones accumulate across the array's + /// lifetime and this function's cost would grow unbounded over time instead of + /// staying proportional to the current live task count. + /// `alive` is allocated once at taskIdList.length() — every removal path removes + /// from taskIdList and tombstones `tasks[id].owner` together (LibCommon.removeTask), + /// so taskIdList's count always equals the number of non-tombstoned entries here, + /// letting this write directly into the final-size array instead of filtering into + /// a scratch buffer first and copying. + /// @return alive The ascending list of currently-alive task IDs. + function buildAliveOrderedTaskIds() private returns (uint256[] memory alive) { + RegistryState storage registryState = LibAppStorage.registryState(); + uint256[] storage ordered = registryState.orderedTaskIds; + uint256 len = ordered.length; + + alive = new uint256[](registryState.taskIdList.length()); + uint256 n; + for (uint256 i = 0; i < len; i++) { + uint256 id = ordered[i]; + if (registryState.tasks[uint64(id)].owner != address(0)) { + alive[n] = id; + n++; + } + } + // Invariant check, not input validation: taskIdList and orderedTaskIds' tombstone + // flags must agree on exactly which tasks are alive (see NatSpec above). A mismatch + // here means the two structures desynced elsewhere — fail loudly rather than + // silently return an array with trailing zero-valued entries. + assert(n == alive.length); + + registryState.orderedTaskIds = alive; + } + + /// @notice Function to update the cycle locked fees, gas committed and tasks lists. + /// @param _lockedFees Updated cycle locked fees + /// @param _sysGasCommittedForNextCycle Updated system gas committed for next cycle + /// @param _gasCommittedForNextCycle Updated gas committed for next cycle + /// @param _gasCommittedForNewCycle Updated gas committed for new cycle + /// @param _state Cycle transition state executing the update. + function updateRegistryState( + uint256 _lockedFees, + uint128 _sysGasCommittedForNextCycle, + uint128 _gasCommittedForNextCycle, + uint128 _gasCommittedForNewCycle, + LibCommon.CycleState _state + ) private { + RegistryState storage registryState = LibAppStorage.registryState(); + + registryState.cycleLockedFees = _lockedFees; + // For this new cycle system tasks commitment is already known as _sysGasCommittedForNextCycle of the previous cycle. + // On suspension every task (UST+GST) has just been removed (see onCycleSuspend), so there + // is no carry-over commitment for the cycle about to start — force it to 0 rather than + // rotating in the pre-wipe accumulated value. + registryState.sysGasCommittedForThisCycle = _state == LibCommon.CycleState.SUSPENDED + ? 0 + : registryState.sysGasCommittedForNextCycle; + registryState.sysGasCommittedForNextCycle = _sysGasCommittedForNextCycle; + registryState.gasCommittedForNextCycle = _gasCommittedForNextCycle; + registryState.gasCommittedForThisCycle = _gasCommittedForNewCycle; + + if (_state == LibCommon.CycleState.FINISHED) { + // transitionState.survivedTaskIds was accumulated incrementally, one + // processTasks batch at a time, in dropOrChargeTasks — so by the time the + // last batch finalizes here, the new cycle's active set is already fully + // determined, ascending, and tombstone-free: registration is blocked while + // a transition is in progress (see the CycleTransitionInProgress guard), so + // nothing can add to taskIdList/orderedTaskIds mid-transition, meaning + // survivedTaskIds ends up exactly equal to taskIdList's remaining contents. + uint256[] memory survivedTaskIds = LibAppStorage.transitionState().survivedTaskIds; + // This single assignment both clears the previous cycle's activeTaskIds (any + // leftover tail elements are zeroed by the compiler when the new array is + // shorter) and writes the new one, without re-scanning taskIdList or paying + // EnumerableSet's extra _positions-mapping SSTORE. + registryState.activeTaskIds = survivedTaskIds; + // Eagerly re-syncs orderedTaskIds to the same list here, for free, since we + // already have it computed — this bounds buildAliveOrderedTaskIds' next-cycle + // filter pass to just this cycle's churn (registrations/removals since this + // point) instead of letting tombstones accumulate across cycle boundaries. + registryState.orderedTaskIds = survivedTaskIds; + } else { + registryState.activeTaskIds = new uint256[](0); + // Every task still in orderedTaskIds at this point was unconditionally + // removed by onCycleSuspend's loop (SUSPENDED means all tasks are dropped), + // so it's now 100% tombstones — clear it eagerly rather than letting the + // next buildAliveOrderedTaskIds call filter through dead weight. + registryState.orderedTaskIds = new uint256[](0); + registryState.sysTaskIds.clear(); + } + } + + /// @notice Function to update the registry configuration with buffered one if exists. + function applyPendingConfig() private returns (bool, uint64) { + AppStorage storage s = LibAppStorage.appStorage(); + + if (!s.ifBufferExists) { + return (false, 0); + } + uint64 pendingCycleDuration = LibAppStorage.bufferConfig().cycleDurationSecs; + s.configuration[LibAppStorage.ACTIVE_CONFIG] = s.configuration[LibAppStorage.BUFFER_CONFIG]; + + s.ifBufferExists = false; + delete s.configuration[LibAppStorage.BUFFER_CONFIG]; + + return (true, pendingCycleDuration); + } + + /// @notice Updates the state of the cycle. + /// @param _state Input state to update cycle state with. + function updateCycleStateTo(LibCommon.CycleState _state) private { + AppStorage storage s = LibAppStorage.appStorage(); + + LibCommon.CycleState oldState = s.cycleState; + s.cycleState = _state; + + emit ICoreFacet.AutomationCycleEvent ( + s.index, + s.cycleState, + s.startTime, + s.durationSecs, + oldState + ); + } + + /// @notice Helper function to update the expected tasks of the transition state. + /// @dev A direct storage-array assignment from `_expectedTasks` both clears any + /// previous contents (the compiler zeroes out any leftover tail elements if + /// the new list is shorter) and writes the new elements in a single pass — + /// one SSTORE per task, field is ever read sequentially(see the declaration) + function updateExpectedTasks(uint256[] memory _expectedTasks) private { + LibAppStorage.transitionState().expectedTasksToBeProcessed = _expectedTasks; + } + + /// @notice Transitions cycle state to the READY state. + function moveToReadyState() private { + // If the cycle duration updated has been identified during transition, then the transition state is kept + // with reset values except new cycle duration to have it properly set for the next new cycle. + // This may happen in case if cycle was ended and feature-flag has been disabled before any task has + // been processed for the cycle transition. + // Note that we want to have consistent data in ready state which says that the cycle pointed in the ready state + // has been finished/summerized, and we are ready to start the next new cycle, and all the cycle information should + // match the finalized/summerized cycle since its start, including cycle duration. + + AppStorage storage s = LibAppStorage.appStorage(); + TransitionState storage transitionState = LibAppStorage.transitionState(); + + // Check if transition state exists + if (s.ifTransitionStateExists) { + if (transitionState.newCycleDuration == s.durationSecs) { + // Delete transition state. Deleting the whole struct already recursively + // clears expectedTasksToBeProcessed since it is a plain dynamic array + delete s.transitionState[LibAppStorage.TRANSITION_STATE]; + s.ifTransitionStateExists = false; + } else { + // Reset all except new cycle duration + transitionState.refundDuration = 0; + transitionState.automationFeePerSec = 0; + transitionState.gasCommittedForNewCycle = 0; + transitionState.gasCommittedForNextCycle = 0; + transitionState.sysGasCommittedForNextCycle = 0; + transitionState.lockedFees = 0; + transitionState.nextTaskIndexPosition = 0; + delete transitionState.expectedTasksToBeProcessed; + delete transitionState.survivedTaskIds; + } + } + updateCycleStateTo(LibCommon.CycleState.READY); + } + + /// @notice Updates the cycle state if the transition is identified to be finalized. + /// As transition happens from suspended state and while transition was in progress + /// - if the feature was enabled back, then the transition will happen direclty to STARTED state, + /// - otherwise the transition will be done to the READY state. + /// + /// In both cases config will be updated. In this case we will make sure to keep the consistency of state + /// when transition to READY state happens through paths + /// - Started -> Suspended -> Ready + /// - or Started-> {Finished, Suspended} -> Ready + /// - or Started -> Finished -> {Started, Suspended} + function updateCycleTransitionStateFromSuspended() private { + AppStorage storage s = LibAppStorage.appStorage(); + + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert ICoreFacet.InvalidRegistryState(); } + if (!isTransitionFinalized()) { + return; + } + + updateRegistryState(0, 0, 0, 0, LibCommon.CycleState.SUSPENDED); + + // Check if automation is enabled + if (s.automationEnabled) { + // Update the config in case if transition flow is STARTED -> SUSPENDED-> STARTED. + // to reflect new configs for the new cycle if it has been updated during SUSPENDED state processing + updateConfigFromBuffer(); + moveToStartedState(); + } else { + moveToReadyState(); + } + } + + /// @notice Marks a task as processed. + /// @param _taskIndex Index of the task to be marked as processed. + function markTaskProcessed(uint64 _taskIndex) private { + TransitionState storage transitionState = LibAppStorage.transitionState(); + + uint64 nextTaskIndexPosition = transitionState.nextTaskIndexPosition; + + if (nextTaskIndexPosition >= transitionState.expectedTasksToBeProcessed.length) { revert ICoreFacet.InconsistentTransitionState(); } + uint64 expectedTask = uint64(transitionState.expectedTasksToBeProcessed[nextTaskIndexPosition]); + + if (expectedTask != _taskIndex) { revert ICoreFacet.OutOfOrderTaskProcessingRequest(); } + transitionState.nextTaskIndexPosition = nextTaskIndexPosition + 1; + } + + /// @notice Updates the cycle state if the transition is identified to be finalized. + /// From FINISHED state we always move to the next cycle in STARTED state first (incrementing cycle index). + /// If automation was disabled during the transition, we immediately initiate suspension from the new STARTED state. + /// This ensures cycle index is always incremented before suspension, and a fresh suspension transition is set up. + function updateCycleTransitionStateFromFinished() private { + AppStorage storage s = LibAppStorage.appStorage(); + + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert ICoreFacet.InvalidRegistryState(); } + + if (isTransitionFinalized()) { + TransitionState storage transitionState = LibAppStorage.transitionState(); + updateRegistryState( + transitionState.lockedFees, + transitionState.sysGasCommittedForNextCycle, + transitionState.gasCommittedForNextCycle, + transitionState.gasCommittedForNewCycle, + LibCommon.CycleState.FINISHED + ); + + // Set current timestamp as cycle start time + // Increment the cycle and update the state to STARTED + moveToStartedState(); + + RegistryState storage registryState = LibAppStorage.registryState(); + if (registryState.activeTaskIds.length > 0) { + uint256[] memory activeTasks = registryState.activeTaskIds; + emit ICoreFacet.ActiveTasks(activeTasks); + } + if (!s.automationEnabled) { + tryMoveToSuspendedState(); + } + } + } + + /// @notice Traverses all input task indexes and either drops or tries to charge automation fee if possible. + /// @param _taskIndexes Input task indexes. + /// @return intermediateState Returns the intermediate state. + function dropOrChargeTasks( + uint256[] memory _taskIndexes + ) private returns (LibCommon.IntermediateStateOfCycleChange memory intermediateState) { + uint64 currentTime = uint64(block.timestamp); + TransitionState storage transitionState = LibAppStorage.transitionState(); + uint64 currentCycleEndTime = currentTime + transitionState.newCycleDuration; + + // Task indexes must arrive pre-sorted ascending — see requireSortedAscending's + // NatSpec for why this contract does not sort them itself. + requireSortedAscending(_taskIndexes); + uint256[] memory taskIndexes = _taskIndexes; + + uint64[] memory removedBuffer = new uint64[](taskIndexes.length); + uint256 removedCount; + + // Process each active task and calculate fee for the cycle for the tasks + for (uint256 i = 0; i < taskIndexes.length; i++) { + uint64 taskId = uint64(taskIndexes[i]); + LibCommon.TransitionResult memory result = dropOrChargeTask( + taskId, + currentTime, + currentCycleEndTime + ); + + if (result.isRemoved) { + removedBuffer[removedCount] = taskId; + removedCount += 1; + } else { + intermediateState.gasCommittedForNextCycle += result.gas; + intermediateState.sysGasCommittedForNextCycle += result.sysGas; + intermediateState.cycleLockedFees += result.fees; + // Accumulate survivors incrementally across every processTasks batch of this + // transition, so updateRegistryState can seed the new cycle's activeTaskIds + // by direct assignment at finalization instead of re-scanning taskIdList — see + // TransitionState.survivedTaskIds and LibCore.updateRegistryState. + transitionState.survivedTaskIds.push(taskId); + } + } + + uint64[] memory removedTasks = new uint64[](removedCount); + for (uint256 j = 0; j < removedCount; j++) { + removedTasks[j] = removedBuffer[j]; + } + intermediateState.removedTasks = removedTasks; + } + + /// @notice Drops or charges the input task. + /// @dev Reverts if `_taskIndex` does not currently exist in the registry: every task + /// index submitted here is expected to come from the caller's own tracking of + /// `expectedTasksToBeProcessed`, so a missing task means the caller has regressed + /// or the registry is in an inconsistent state, and either should be surfaced + /// immediately rather than silently treated as a no-op. + /// @param _taskIndex Task index to be dropped or charged. + /// @param _currentTime Current time. + /// @param _currentCycleEndTime End time of the current cycle. + /// @return result Returns the TransitionResult. + function dropOrChargeTask( + uint64 _taskIndex, + uint64 _currentTime, + uint64 _currentCycleEndTime + ) private returns (LibCommon.TransitionResult memory result) { + require(LibCommon.ifTaskExists(_taskIndex), ICoreFacet.UnknownTaskToProcess(_taskIndex)); + markTaskProcessed(_taskIndex); + + TaskMetadataLW memory task = LibCommon.getTaskLW(_taskIndex); + bool isUst = task.taskType == LibCommon.TaskType.UST; + + RegistryState storage registryState = LibAppStorage.registryState(); + + // Task is cancelled or expired + if (task.taskState == LibCommon.TaskState.CANCELLED || _currentTime >= task.expiryTime) { + if (isUst) { + LibAccounting.refundDepositAndDrop(_taskIndex, task.owner, task.depositFee, task.depositFee); + } else { + // Remove the task from registry and system registry + LibCommon.removeTask(_taskIndex, task.owner, true, false); + } + result.isRemoved = true; + } else if (!isUst) { + // Active GST + // Governance submitted tasks are not charged + + if (task.expiryTime > _currentCycleEndTime) { + result.sysGas = task.maxGasAmount; + } + registryState.tasks[_taskIndex].taskState = LibCommon.TaskState.ACTIVE; + } else { + TransitionState storage transitionState = LibAppStorage.transitionState(); + // Active UST + uint128 fee = LibAccounting.calculateTaskFee( + task.taskState, + task.expiryTime, + task.maxGasAmount, + transitionState.newCycleDuration, + _currentTime, + transitionState.automationFeePerSec + ); + + // If the task reached this phase that means it is a valid active task for the new cycle. + // During cleanup all expired tasks has been removed from the registry but the state of the tasks is not updated. + // As here we need to distinguish new tasks from already existing active tasks, + // as the fee calculation for them will be different based on their active duration in the cycle. + // For more details see calculateTaskFee function. + + registryState.tasks[_taskIndex].taskState = LibCommon.TaskState.ACTIVE; + (result.isRemoved, result.gas, result.fees) = tryWithdrawTaskAutomationFee( + _taskIndex, + task.owner, + task.maxGasAmount, + task.expiryTime, + task.depositFee, + fee, + _currentCycleEndTime, + task.automationFeeCapForCycle, + task.txHash + ); + } + } + + /// @notice Helper function to withdraw automation task fees for an active task. + /// @param _taskIndex Index of the task. + /// @param _owner Owner of the task. + /// @param _maxGasAmount Max gas amount of the task. + /// @param _expiryTime Expiry time of the task. + /// @param _depositRefund Deposit refund of the task. + /// @param _fee Fees to be charged for the task. + /// @param _currentCycleEndTime End time of the current cycle. + /// @param _automationFeeCapForCycle Max automation fee for a cycle to be paid. + /// @param _regHash Tx hash of the task. + /// @return Bool representing if the task was removed. + /// @return Amount to add to gasCommittedForNextCycle + /// @return Amount to add to cycleLockedFees + function tryWithdrawTaskAutomationFee( + uint64 _taskIndex, + address _owner, + uint128 _maxGasAmount, + uint64 _expiryTime, + uint128 _depositRefund, + uint128 _fee, + uint64 _currentCycleEndTime, + uint128 _automationFeeCapForCycle, + bytes32 _regHash + ) private returns (bool, uint128, uint128) { + AppStorage storage s = LibAppStorage.appStorage(); + + // Remove the automation task if the cycle fee cap is exceeded. + // It might happen that task has been expired by the time charging is being done. + // This may be caused by the fact that bookkeeping transactions has been withheld due to cycle transition. + + bool isRemoved; + uint128 gas; + uint128 fees; + if (_fee > _automationFeeCapForCycle) { + LibAccounting.refundDepositAndDrop(_taskIndex, _owner, _depositRefund, _depositRefund); + + isRemoved = true; + + emit ICoreFacet.TaskCancelledCapacitySurpassed( + _taskIndex, + _owner, + _fee, + _automationFeeCapForCycle, + _regHash + ); + } else { + address erc20Supra = s.erc20Supra; + uint256 userBalance = IERC20(erc20Supra).balanceOf(_owner); + uint256 allowance = IERC20(erc20Supra).allowance(_owner, address(this)); + + if (userBalance < _fee || allowance < _fee) { + // If the user hasn't granted enough allowance or if they don't have enough balance, remove the task. + // DON'T refund the locked deposit, but simply unlock it and emit an event. + + LibAccounting.safeUnlockLockedDeposit(_taskIndex, _depositRefund); + LibCommon.removeTask(_taskIndex, _owner, false, false); + + isRemoved = true; + + emit ICoreFacet.TaskCancelledInsufficentBalanceAllowance( + _taskIndex, + _owner, + _fee, + userBalance, + allowance, + _regHash + ); + } else { + if (_fee != 0) { + // Charge the fee + bool sent = IERC20(erc20Supra).transferFrom(_owner, address(this), _fee); + if (!sent) { revert ICoreFacet.TransferFailed(); } + + fees = _fee; + } + + emit ICoreFacet.TaskCycleFeeWithdraw( + s.index, + _taskIndex, + _owner, + _fee + ); + + // Calculate gas commitment for the next cycle only for valid active tasks + if (_expiryTime > _currentCycleEndTime) { + gas = _maxGasAmount; + } + } + } + + return (isRemoved, gas, fees); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: INTERNAL FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Checks if the cycle transition is finalized. + /// @return Bool representing if the cycle transition is finalized. + function isTransitionFinalized() internal view returns (bool) { + TransitionState storage transitionState = LibAppStorage.transitionState(); + return transitionState.expectedTasksToBeProcessed.length == transitionState.nextTaskIndexPosition; + } + + /// @notice Checks if the cycle transition is in progress. + /// @return Bool representing if the cycle transition is in progress. + function isTransitionInProgress() internal view returns (bool) { + return LibAppStorage.transitionState().nextTaskIndexPosition != 0; + } + + /// @notice Traverses the list of the tasks and based on the task state and expiry information either charges or drops the task after refunding eligable fees. + /// Tasks are checked not to be processed more than once. + /// This function should be called only if registry is in FINISHED state, meaning a normal cycle transition is happening. + /// After processing all input tasks, intermediate transition state is updated and transition end is checked (whether all expected tasks has been processed already). + /// In case if transition end is detected a start of the new cycle is given (if during trasition period suspention is not requested) and corresponding event is emitted. + /// @param _cycleIndex Cycle index of the new cycle to which the transition is being done. + /// @param _taskIndexes Array of task indexes to be processed. + function onCycleTransition(uint64 _cycleIndex, uint256[] memory _taskIndexes) internal { + AppStorage storage s = LibAppStorage.appStorage(); + + if (s.cycleState != LibCommon.CycleState.FINISHED) { revert ICoreFacet.InvalidRegistryState(); } + + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert ICoreFacet.InvalidRegistryState(); } + if (s.index + 1 != _cycleIndex) { revert ICoreFacet.InvalidInputCycleIndex(); } + + LibCommon.IntermediateStateOfCycleChange memory intermediateState = dropOrChargeTasks(_taskIndexes); + + TransitionState storage transitionState = LibAppStorage.transitionState(); + transitionState.lockedFees += intermediateState.cycleLockedFees; + transitionState.gasCommittedForNextCycle += intermediateState.gasCommittedForNextCycle; + transitionState.sysGasCommittedForNextCycle += intermediateState.sysGasCommittedForNextCycle; + + updateCycleTransitionStateFromFinished(); + if (intermediateState.removedTasks.length > 0) { + emit ICoreFacet.RemovedTasks(intermediateState.removedTasks); + } + } + + /// @notice Traverses the list of the tasks and refunds automation(if not PENDING) and deposit fees for all tasks and removes from registry. + /// This function is called only if automation feature is disabled, i.e. cycle is in SUSPENDED state. + /// After processing input set of tasks the end of suspention process is checked(i.e. all expected tasks have been processed). + /// In case if end is identified, the registry state is update to READY and corresponding event is emitted. + /// @param _cycleIndex Input cycle index of the cycle being suspended. + /// @param _taskIndexes Array of task indexes to be processed. + function onCycleSuspend(uint64 _cycleIndex, uint256[] memory _taskIndexes) internal { + AppStorage storage s = LibAppStorage.appStorage(); + + if (s.cycleState != LibCommon.CycleState.SUSPENDED) { revert ICoreFacet.InvalidRegistryState(); } + if (s.index != _cycleIndex) { revert ICoreFacet.InvalidInputCycleIndex(); } + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert ICoreFacet.InvalidRegistryState(); } + + uint64 currentTime = uint64(block.timestamp); + + // Task indexes must arrive pre-sorted ascending — see requireSortedAscending's + // NatSpec for why this contract does not sort them itself. + requireSortedAscending(_taskIndexes); + uint256[] memory taskIndexes = _taskIndexes; + uint64[] memory removedTasks = new uint64[](taskIndexes.length); + + uint64 removedCounter; + for (uint i = 0; i < taskIndexes.length; i++) { + uint64 taskId = uint64(taskIndexes[i]); + // Every task index submitted here is expected to come from the caller's own + // tracking of expectedTasksToBeProcessed, so a missing task means the caller + // has regressed or the registry is in an inconsistent state — surface that + // immediately rather than silently skipping it (see dropOrChargeTask). + require(LibCommon.ifTaskExists(taskId), ICoreFacet.UnknownTaskToProcess(taskId)); + TaskMetadataLW memory task = LibCommon.getTaskLW(taskId); + + LibCommon.removeTask(taskId, task.owner, false, false); + + removedTasks[removedCounter++] = taskId; + markTaskProcessed(taskId); + + // Nothing to refund for GST tasks + if (task.taskType == LibCommon.TaskType.UST) { + TransitionState storage transitionState = LibAppStorage.transitionState(); + LibAccounting.refundTaskFees( + currentTime, + transitionState.refundDuration, + transitionState.automationFeePerSec, + task + ); + } + } + + updateCycleTransitionStateFromSuspended(); + + if (removedCounter > 0) { + // Emit only the entries actually removed. + emit ICoreFacet.RemovedTasks(removedTasks); + } + } + + /// @notice Removes a registered task when predicate validation fails during runtime. + /// @param _taskId Task index that failed predicate validation. + /// @param _cycleEndTime Cycle end time. + /// @param _currentTime Current time. + /// @param _residualInterval Residual interval. + /// @param _reason Reason for task removal. + function handleTasksRemoval( + uint64 _taskId, + uint64 _cycleEndTime, + uint64 _currentTime, + uint64 _residualInterval, + string memory _reason + ) internal returns (LibCommon.RemovedTask memory removedTask) { + RegistryState storage registryState = LibAppStorage.registryState(); + + TaskMetadataLW memory task = LibCommon.toLW(registryState.tasks[_taskId]); + bool isGst = task.taskType == LibCommon.TaskType.GST; + + (uint128 cycleFeeRefund, uint128 depositRefund) = LibRegistry.removeTaskAndComputeRefund( + _taskId, + _cycleEndTime, + _currentTime, + _residualInterval, + task.expiryTime, + task.maxGasAmount, + task.depositFee, + task.owner, + task.taskState, + isGst + ); + + if (!isGst) { + LibAccounting.refund(task.owner, (cycleFeeRefund + depositRefund)); + } + + removedTask = LibCommon.RemovedTask(_taskId, task.taskType, task.owner, task.txHash, _reason); + } + + /// @notice Helper function called when cycle end is identified. + function onCycleEndInternal() internal { + AppStorage storage s = LibAppStorage.appStorage(); + + if (!s.automationEnabled) { + tryMoveToSuspendedState(); + } else { + if (totalTasks() == 0) { + // Registry is empty update config buffer and move to STARTED state directly + updateConfigFromBuffer(); + moveToStartedState(); + } else { + // buildAliveOrderedTaskIds is O(n) regardless of removal history — see its + // NatSpec for why sorting a taskIdList-derived snapshot is not safe here. + uint256[] memory expectedTasksToBeProcessed = buildAliveOrderedTaskIds(); + + // Updates transition state + TransitionState storage transitionState = LibAppStorage.transitionState(); + + transitionState.refundDuration = 0; + transitionState.newCycleDuration = s.durationSecs; + transitionState.gasCommittedForNewCycle = LibAppStorage.registryState().gasCommittedForNextCycle; + transitionState.gasCommittedForNextCycle = 0; + transitionState.sysGasCommittedForNextCycle = 0; + transitionState.lockedFees = 0; + transitionState.nextTaskIndexPosition = 0; + // Must be cleared explicitly: unlike expectedTasksToBeProcessed (a full + // reassignment below), survivedTaskIds is built up via .push() in + // dropOrChargeTasks across this transition's batches, so any leftover + // entries from a prior transition must not carry over. + delete transitionState.survivedTaskIds; + updateExpectedTasks(expectedTasksToBeProcessed); + + s.ifTransitionStateExists = true; + + // During cycle transition we update config only after transition state is created in order to have new cycle duration as transition state parameter. + updateConfigFromBuffer(); + + // Calculate automation fee per second for the new cycle only after configuration is updated. + // As we already know the committed gas for the new cycle it is being calculated using updated fee parameters + // and will be used to charge tasks during transition process. + transitionState.automationFeePerSec = LibAccounting.calculateAutomationFeeMultiplierForCommittedOccupancy(transitionState.gasCommittedForNewCycle); + updateCycleStateTo(LibCommon.CycleState.FINISHED); + } + } + } + + /// @notice Transition to suspended state is expected to be called + /// a) when cycle is active and in progress + /// - here we simply move to suspended state so native layer can start requesting tasks processing + /// which will end up in refunds and cleanup. Note that refund will be done based on total gas-committed + /// for the current cycle defined at the begining for the cycle, and using current automation fee parameters + /// b) when cycle has just finished and there was another transaction causing feature suspension + /// - as this both events happen in scope of the same block, then we will simply update the state to suspended + /// and the native layer should identify the transition and request processing of the all available tasks. + /// Note that in this case automation fee refund will not be expected and suspention and cycle end matched and + /// no fee was yet charged to be refunded. + /// So the duration for refund and automation-fee-per-second for refund will be 0 + /// c) when cycle transition was in progress and there was a feature suspension, but it could not be applied, + /// and postponed till the cycle transition concludes + /// In all the cases if there are no tasks in registry the state will be updated directly to READY state. + function tryMoveToSuspendedState() internal { + AppStorage storage s = LibAppStorage.appStorage(); + TransitionState storage transitionState = LibAppStorage.transitionState(); + + if (totalTasks() == 0) { + // Registry is empty move to ready state directly + updateCycleStateTo(LibCommon.CycleState.READY); + } else if (!s.ifTransitionStateExists) { + // Indicates that cycle was in STARTED state when suspention has been identified. + // It is safe to assert that cycleEndTime will always be greater than current chain time as + // the cycle end is check in the block metadata txn execution which proceeds any other transaction in the block. + // Including the transaction which caused transition to suspended state. + // So in case if cycleEndTime < currentTime then cycle end would have been identified + // and we would have enterend else branch instead. + // This holds true even if we identified suspention when moving from FINALIZED->STARTED state. + // As in this case we will first transition to the STARTED state and only then to SUSPENDED. + // And when transition to STARTED state we update the cycle start-time to be the current-chain-time. + uint64 currentTime = uint64(block.timestamp); + uint64 cycleEndTime = LibCommon.getCycleEndTime(); + + if (currentTime < s.startTime) { revert ICoreFacet.InvalidRegistryState(); } + if (currentTime >= cycleEndTime) { revert ICoreFacet.InvalidRegistryState(); } + if (!LibCommon.isCycleStarted()) { revert ICoreFacet.InvalidRegistryState(); } + + uint256[] memory expectedTasksToBeProcessed = buildAliveOrderedTaskIds(); + + transitionState.refundDuration = cycleEndTime - currentTime; + transitionState.newCycleDuration = s.durationSecs; + transitionState.automationFeePerSec = LibAccounting.calculateAutomationFeeMultiplierForCurrentCycle(); + transitionState.gasCommittedForNewCycle = 0; + transitionState.gasCommittedForNextCycle = 0; + transitionState.sysGasCommittedForNextCycle = 0; + transitionState.lockedFees = 0; + transitionState.nextTaskIndexPosition = 0; + // Defensive parity with onCycleEndInternal's setup — this branch only runs + // when no transition is already in progress, so survivedTaskIds should + // already be empty, but this guarantees it regardless of how the prior + // transition (if any) concluded. + delete transitionState.survivedTaskIds; + + updateExpectedTasks(expectedTasksToBeProcessed); + s.ifTransitionStateExists = true; + + updateCycleStateTo(LibCommon.CycleState.SUSPENDED); + } else { + if (s.cycleState != LibCommon.CycleState.FINISHED) { revert ICoreFacet.InvalidRegistryState(); } + if (isTransitionInProgress()) { revert ICoreFacet.InvalidRegistryState(); } + + // Did not manage to charge cycle fee, so automationFeePerSec will be 0 along with remaining duration + // So the tasks sent for refund, will get only deposit refunded. + transitionState.refundDuration = 0; + transitionState.automationFeePerSec = 0; + transitionState.gasCommittedForNewCycle = 0; + + updateCycleStateTo(LibCommon.CycleState.SUSPENDED); + } + } + + /// @notice Transitions cycle state to the STARTED state. + function moveToStartedState() internal { + AppStorage storage s = LibAppStorage.appStorage(); + + s.index += 1; + s.startTime = uint64(block.timestamp); + + // Check if the transition state exists + if (s.ifTransitionStateExists) { + TransitionState storage transitionState = LibAppStorage.transitionState(); + s.durationSecs = transitionState.newCycleDuration; + s.ifTransitionStateExists = false; + // Deleting the whole struct already recursively clears expectedTasksToBeProcessed + // since it is a plain dynamic array — no separate reset needed. + delete s.transitionState[LibAppStorage.TRANSITION_STATE]; + } + + updateCycleStateTo(LibCommon.CycleState.STARTED); + } + + /// @notice Function to update the registry config structure with values extracted from the buffer, if the buffer exists. + function updateConfigFromBuffer() internal { + AppStorage storage s = LibAppStorage.appStorage(); + + (bool applied, uint64 cycleDuration) = applyPendingConfig(); + if (!applied) return; + + // Check if transition state exists + if (s.ifTransitionStateExists) { + LibAppStorage.transitionState().newCycleDuration = cycleDuration; + } else { + s.durationSecs = cycleDuration; + } + } +} diff --git a/solidity/supra_contracts/src/libraries/LibDiamond.sol b/solidity/supra_contracts/src/libraries/LibDiamond.sol new file mode 100644 index 0000000000..b62ac83c1e --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibDiamond.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +/******************************************************************************/ +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; +import { LibUtils } from "./LibUtils.sol"; + +// Remember to add the loupe functions from DiamondLoupeFacet to the diamond. +// The loupe functions are required by the EIP2535 Diamonds standard + +library LibDiamond { + using LibUtils for address; + + // 32 bytes keccak hash of a string to use as a diamond storage location. + bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.registry.automation.storage"); + + // Custom errors + error AddressCannotBeZero(); + error AddressMustBeZero(); + error AddressNotAContract(); + error CannotRemoveImmutableFunction(); + error CannotReplaceFunctionWithSameFunction(); + error FunctionAlreadyExists(); + error FunctionDoesNotExist(); + error IncorrectFacetCutAction(); + error InitializationFunctionReverted(address _initializationContractAddress, bytes _calldata); + error MustBeContractOwner(); + error NoSelectorsInFacetToCut(); + + struct FacetAddressAndPosition { + address facetAddress; + uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors array + } + + struct FacetFunctionSelectors { + bytes4[] functionSelectors; + uint256 facetAddressPosition; // position of facetAddress in facetAddresses array + } + + struct DiamondStorage { + // maps function selector to the facet address and + // the position of the selector in the facetFunctionSelectors.selectors array + mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition; + // maps facet addresses to function selectors + mapping(address => FacetFunctionSelectors) facetFunctionSelectors; + // facet addresses + address[] facetAddresses; + // Used to query if a contract implements an interface. + // Used to implement ERC-165. + mapping(bytes4 => bool) supportedInterfaces; + // owner of the contract + address contractOwner; + // Set once, at the end of DiamondInit.init(), and read by + // DiamondLoupeFacet.isInitialized() (IRegistryStatus). A dedicated flag rather than + // reusing `supportedInterfaces`: the two are unrelated concerns (ERC-165 answers "which + // standards does this diamond implement", not "has genesis initialization completed"), + // and coupling them would mean any future change to ERC-165 registration has to also + // reason about whether it disturbs the isInitialized() signal. A dedicated bool makes + // that signal read directly off its own state instead of being reconstructed from an + // invariant ("these interfaces are always registered together") a reader has to verify. + bool initialized; + } + + function diamondStorage() internal pure returns (DiamondStorage storage ds) { + bytes32 position = DIAMOND_STORAGE_POSITION; + // assigns struct storage slot to the storage position + assembly { + ds.slot := position + } + } + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function setContractOwner(address _newOwner) internal { + assertNonZeroAddress(_newOwner); + DiamondStorage storage ds = diamondStorage(); + address previousOwner = ds.contractOwner; + ds.contractOwner = _newOwner; + emit OwnershipTransferred(previousOwner, _newOwner); + } + + function contractOwner() internal view returns (address contractOwner_) { + contractOwner_ = diamondStorage().contractOwner; + } + + function enforceIsContractOwner() internal view { + if (msg.sender != diamondStorage().contractOwner) { revert MustBeContractOwner(); } + } + + event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata); + + // Internal function version of diamondCut + function diamondCut( + IDiamondCut.FacetCut[] memory _diamondCut, + address _init, + bytes memory _calldata + ) internal { + for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) { + IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action; + if (action == IDiamondCut.FacetCutAction.Add) { + addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Replace) { + replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else if (action == IDiamondCut.FacetCutAction.Remove) { + removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors); + } else { + revert IncorrectFacetCutAction(); + } + } + initializeDiamondCut(_init, _calldata); + emit DiamondCut(_diamondCut, _init, _calldata); + } + + function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + assertNonEmptySelectors(_functionSelectors); + DiamondStorage storage ds = diamondStorage(); + assertNonZeroAddress(_facetAddress); + uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length); + // add new facet address if it does not exist + if (selectorPosition == 0) { + addFacet(ds, _facetAddress); + } + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + if (oldFacetAddress != address(0)) { revert FunctionAlreadyExists(); } + addFunction(ds, selector, selectorPosition, _facetAddress); + selectorPosition++; + } + } + + function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + assertNonEmptySelectors(_functionSelectors); + DiamondStorage storage ds = diamondStorage(); + assertNonZeroAddress(_facetAddress); + uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length); + // add new facet address if it does not exist + if (selectorPosition == 0) { + addFacet(ds, _facetAddress); + } + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + if (oldFacetAddress == _facetAddress) { revert CannotReplaceFunctionWithSameFunction(); } + removeFunction(ds, oldFacetAddress, selector); + addFunction(ds, selector, selectorPosition, _facetAddress); + selectorPosition++; + } + } + + // NOTE: CoreFacet.processTasks, CoreFacet.monitorCycleEnd and + // CoreFacet.removeRegisteredTask are called every block/cycle by the node's + // off-chain VM-signer decoder, which hardcodes their selectors. + // The following functions are also utilized by the node's off-chain logic to manage automation registry at runtime + // And should not be removed unless node binary and automation registry runtime management is updated accordingly: + // - DiamondLoupeFacet::isInitialized + // - CoreFacet::getCycleStateDetails + // - RegistryFacet::getTaskDetails + // - RegistryFacet::getTaskIdList + // - RegistryFacet::getActiveTaskIds + // - CoreFacet::isAutomationReadyEnabled + // Whoever operates diamondCut post-genesis must never submit a Remove action for + // these selectors (Replace, to ship a fix, is fine and unaffected by + // this note) — see CoreFacet.sol for the selector list. + function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + assertNonEmptySelectors(_functionSelectors); + DiamondStorage storage ds = diamondStorage(); + // EIP-2535 convention: a Remove action must pass the zero address as the facet. + if (_facetAddress != address(0)) { revert AddressMustBeZero(); } + for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) { + bytes4 selector = _functionSelectors[selectorIndex]; + address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress; + removeFunction(ds, oldFacetAddress, selector); + } + } + + function addFacet(DiamondStorage storage ds, address _facetAddress) internal { + assertIsContract(_facetAddress); + ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = ds.facetAddresses.length; + ds.facetAddresses.push(_facetAddress); + } + + + function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _selectorPosition, address _facetAddress) internal { + ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition; + ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(_selector); + ds.selectorToFacetAndPosition[_selector].facetAddress = _facetAddress; + } + + function removeFunction(DiamondStorage storage ds, address _facetAddress, bytes4 _selector) internal { + if (_facetAddress == address(0)) { revert FunctionDoesNotExist(); } + // an immutable function is a function defined directly in a diamond + if (_facetAddress == address(this)) { revert CannotRemoveImmutableFunction(); } + // replace selector with last selector, then delete last selector + uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition; + uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facetAddress].functionSelectors.length - 1; + // if not the same then replace _selector with lastSelector + if (selectorPosition != lastSelectorPosition) { + bytes4 lastSelector = ds.facetFunctionSelectors[_facetAddress].functionSelectors[lastSelectorPosition]; + ds.facetFunctionSelectors[_facetAddress].functionSelectors[selectorPosition] = lastSelector; + ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition); + } + // delete the last selector + ds.facetFunctionSelectors[_facetAddress].functionSelectors.pop(); + delete ds.selectorToFacetAndPosition[_selector]; + + // if no more selectors for facet address then delete the facet address + if (lastSelectorPosition == 0) { + // replace facet address with last facet address and delete last facet address + uint256 lastFacetAddressPosition = ds.facetAddresses.length - 1; + uint256 facetAddressPosition = ds.facetFunctionSelectors[_facetAddress].facetAddressPosition; + if (facetAddressPosition != lastFacetAddressPosition) { + address lastFacetAddress = ds.facetAddresses[lastFacetAddressPosition]; + ds.facetAddresses[facetAddressPosition] = lastFacetAddress; + ds.facetFunctionSelectors[lastFacetAddress].facetAddressPosition = facetAddressPosition; + } + ds.facetAddresses.pop(); + delete ds.facetFunctionSelectors[_facetAddress].facetAddressPosition; + } + } + + function initializeDiamondCut(address _init, bytes memory _calldata) internal { + if (_init == address(0)) { + return; + } + assertIsContract(_init); + (bool success, bytes memory error) = _init.delegatecall(_calldata); + if (!success) { + if (error.length > 0) { + // bubble up error + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(error) + revert(add(32, error), returndata_size) + } + } else { + revert InitializationFunctionReverted(_init, _calldata); + } + } + } + + /// @notice Assert that the given address is not zero address + /// @param _addr The address to check + function assertNonZeroAddress(address _addr) private pure { + if (_addr == address(0)) { revert AddressCannotBeZero(); } + } + + /// @notice Assert that the given selectors array is not empty + /// @param _selectors The selectors array to check + function assertNonEmptySelectors(bytes4[] memory _selectors) private pure { + if (_selectors.length == 0) { revert NoSelectorsInFacetToCut(); } + } + + /// @notice Assert that the given address is a contract + /// @param _addr The address to check + function assertIsContract(address _addr) private view { + if (!_addr.isContract()) { revert AddressNotAContract(); } + } +} diff --git a/solidity/supra_contracts/src/libraries/LibDiamondUtils.sol b/solidity/supra_contracts/src/libraries/LibDiamondUtils.sol new file mode 100644 index 0000000000..467a669cec --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibDiamondUtils.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Diamond} from "../Diamond.sol"; +import {DiamondCutFacet} from "../facets/DiamondCutFacet.sol"; +import {DiamondLoupeFacet} from "../facets/DiamondLoupeFacet.sol"; +import {OwnershipFacet} from "../facets/OwnershipFacet.sol"; +import {ConfigFacet} from "../facets/ConfigFacet.sol"; +import {RegistryFacet} from "../facets/RegistryFacet.sol"; +import {CoreFacet} from "../facets/CoreFacet.sol"; +import {DiamondInit} from "../upgradeInitializers/DiamondInit.sol"; +import {FacetsDeployment, InitParams} from "../libraries/DiamondTypes.sol"; + +struct Deployment { + FacetsDeployment facets; + address diamond; +} + +library LibDiamondUtils { + + // ============================================================= + // DEFAULT INIT CONFIG + // ============================================================= + + function defaultInitParams() internal pure returns (InitParams memory p) { + p = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 160, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 40, + registrationEnabled: true, + automationEnabled: true + }); + } + + // ============================================================= + // DEPLOY FUNCTION + // ============================================================= + + /// @notice Deploys all facets, DiamondInit, and a fully-initialized Diamond + /// in a single call. The Diamond constructor applies all facet cuts + /// and runs DiamondInit atomically. + function deploy( + address _owner, + address _erc20Supra, + InitParams memory _params + ) internal returns (Deployment memory d) { + d.facets = deployFacets(); + d.diamond = address (new Diamond(_owner, d.facets, _erc20Supra, _params)); + } + + /// @notice Deploys all facets, DiamondInit. + function deployFacets() internal returns (FacetsDeployment memory d) { + + // 1) Deploy DiamondCutFacet + d.diamondCutFacet = address(new DiamondCutFacet()); + + // 2) Deploy remaining facets + d.loupeFacet = address(new DiamondLoupeFacet()); + d.ownershipFacet = address(new OwnershipFacet()); + d.configFacet = address(new ConfigFacet()); + d.registryFacet = address(new RegistryFacet()); + d.coreFacet = address(new CoreFacet()); + + // 3) Deploy DiamondInit + d.diamondInit = address(new DiamondInit()); + } +} diff --git a/solidity/supra_contracts/src/libraries/LibRegistry.sol b/solidity/supra_contracts/src/libraries/LibRegistry.sol new file mode 100644 index 0000000000..d982d8bc19 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibRegistry.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {LibAccounting} from "./LibAccounting.sol"; +import {LibCommon} from "./LibCommon.sol"; +import {LibUtils} from "./LibUtils.sol"; +import {AppStorage, Config, LibAppStorage, RegistryState, TaskMetadata, TaskMetadataLW} from "./LibAppStorage.sol"; +import {IRegistryFacet} from "../interfaces/IRegistryFacet.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +library LibRegistry { + using LibUtils for address; + using EnumerableSet for EnumerableSet.UintSet; + + /// @notice Address of the transaction hash precompile. + address public constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: PRIVATE FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function to validate the task duration. + function validateTaskDuration( + uint64 _regTime, + uint64 _expiryTime, + uint64 _taskDurationCap, + uint64 _cycleEndTime + ) private pure { + if (_expiryTime <= _regTime) { revert IRegistryFacet.InvalidExpiryTime(); } + + uint64 taskDuration = _expiryTime - _regTime; + if (taskDuration > _taskDurationCap) { revert IRegistryFacet.InvalidTaskDuration(); } + + if ( _expiryTime <= _cycleEndTime) { revert IRegistryFacet.TaskExpiresBeforeNextCycle(); } + } + + /// @notice Helper function to validate the inputs while registering a task. + function validateInputs(bytes memory _payloadTx, uint128 _maxGasAmount) private view { + if (_payloadTx.length > LibAppStorage.appStorage().maxPayloadLength) revert IRegistryFacet.PayloadTooLarge(); + + ( , address payloadTarget, bytes memory payload, ) = abi.decode(_payloadTx, (uint128, address, bytes, LibCommon.AccessListEntry[])); + payloadTarget.validateContractAddress(); + if (payload.length < 4) revert IRegistryFacet.InvalidPayloadLength(); + + if (_maxGasAmount == 0) { revert IRegistryFacet.InvalidMaxGasAmount(); } + } + + /// @notice Helper function to validate the combined size of a task's auxData entries. + function validateAuxData(bytes[] memory _auxData) private view { + AppStorage storage s = LibAppStorage.appStorage(); + if (_auxData.length > s.maxAuxDataEntries) revert IRegistryFacet.AuxDataTooLarge(); + + uint256 totalLength; + for (uint256 i = 0; i < _auxData.length; i++) { + totalLength += _auxData[i].length; + } + if (totalLength > s.maxAuxDataLength) revert IRegistryFacet.AuxDataTooLarge(); + } + + /// @notice Read tx hash via precompile. Reverts if precompile missing/fails. + function readTxHash() private view returns (bytes32) { + (bool ok, bytes memory out) = TX_HASH_PRECOMPILE.staticcall(""); + require(ok, IRegistryFacet.FailedToCallTxHashPrecompile()); + require(out.length == 32, IRegistryFacet.TxnHashLengthShouldBe32(uint64(out.length))); + return abi.decode(out, (bytes32)); + } + + function validateOwnerType( + address _owner, + LibCommon.TaskType _taskType, + bool _isGst + ) private view { + // Check if authorised + if (msg.sender != _owner) { revert IRegistryFacet.UnauthorizedAccount(); } + + // Enforce task type + if (_isGst) { + if (_taskType == LibCommon.TaskType.UST) { + revert IRegistryFacet.UnsupportedTaskOperation(); + } + } else { + if (_taskType == LibCommon.TaskType.GST) { + revert IRegistryFacet.UnsupportedTaskOperation(); + } + } + } + + /// @notice Validates a predicate by calling it and checking the return value. + /// @param _predicate Predicate to validate + /// @param gasAmount Gas amount to use for the static call + function validatePredicate(bytes memory _predicate, uint128 gasAmount) private view { + if (_predicate.length > LibAppStorage.appStorage().maxPredicateLength) revert IRegistryFacet.PredicateTooLarge(); + + (address payloadTarget, bytes memory payload) = abi.decode(_predicate, (address, bytes)); + payloadTarget.validateContractAddress(); + if (payload.length < 4) revert IRegistryFacet.InvalidPayloadLength(); + + // The predicate must succeed within the task's max-gas-amount. + (bool success, bytes memory data) = payloadTarget.staticcall{gas: gasAmount}(payload); + if (!success) revert IRegistryFacet.StaticCallToPredicateFailed(); + if (data.length != 32) revert IRegistryFacet.InvalidReturnLengthOfPredicate(); + + uint256 val = abi.decode(data, (uint256)); + if (val > 1) revert IRegistryFacet.InvalidReturnTypeOfPredicate(); + } + + /// @notice Helper function that performs validation and updates state for a valid task. + function updateStateForValidRegistration( + uint256 _totalTasks, + uint64 _regTime, + uint64 _expiryTime, + bytes memory _payloadTx, + bytes memory _predicate, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + bool _isUst + ) private { + AppStorage storage s = LibAppStorage.appStorage(); + Config storage activeConfig = LibAppStorage.activeConfig(); + RegistryState storage registryState = LibAppStorage.registryState(); + + // Check if automation and registration is enabled + if (!s.automationEnabled) { revert IRegistryFacet.AutomationNotEnabled(); } + if (!s.registrationEnabled) { revert IRegistryFacet.RegistrationDisabled(); } + + if (!LibCommon.isCycleStarted()) { revert IRegistryFacet.CycleTransitionInProgress(); } + + // Validate inputs (including the _maxGasAmount == 0 check) before validatePredicate, + // since validatePredicate's staticcall is gas-limited to _maxGasAmount: with + // _maxGasAmount == 0 it would fail with StaticCallToPredicateFailed and mask the more + // specific InvalidMaxGasAmount error. + validateInputs(_payloadTx, _maxGasAmount); + validatePredicate(_predicate, _maxGasAmount); + + uint64 taskDurationCap; + uint128 gasCommittedForNextCycle; + uint128 nextCycleRegistryMaxGasCap; + if (_isUst) { + if (_totalTasks >= activeConfig.taskCapacity) { revert IRegistryFacet.TaskCapacityReached(); } + if (_gasPriceCap == 0) { revert IRegistryFacet.InvalidGasPriceCap(); } + + gasCommittedForNextCycle = registryState.gasCommittedForNextCycle; + uint128 estimatedAutomationFeeForCycle = LibAccounting.estimateAutomationFeeWithCommittedOccupancyInternal(_maxGasAmount, gasCommittedForNextCycle); + if (_automationFeeCapForCycle < estimatedAutomationFeeForCycle) { revert IRegistryFacet.InsufficientFeeCapForCycle(estimatedAutomationFeeForCycle); } + taskDurationCap = activeConfig.taskDurationCapSecs; + nextCycleRegistryMaxGasCap = registryState.nextCycleRegistryMaxGasCap; + } else { + if (_totalTasks >= activeConfig.sysTaskCapacity) { revert IRegistryFacet.TaskCapacityReached(); } + + gasCommittedForNextCycle = registryState.sysGasCommittedForNextCycle; + taskDurationCap = activeConfig.sysTaskDurationCapSecs; + nextCycleRegistryMaxGasCap = registryState.nextCycleSysRegistryMaxGasCap; + } + + validateTaskDuration(_regTime, _expiryTime, taskDurationCap, s.startTime + s.durationSecs); + + uint128 gasCommitted = _maxGasAmount + gasCommittedForNextCycle; + if (gasCommitted > nextCycleRegistryMaxGasCap) { revert IRegistryFacet.GasCommittedExceedsMaxGasCap(); } + + if (_isUst) { + registryState.gasCommittedForNextCycle = gasCommitted; + } else { + registryState.sysGasCommittedForNextCycle = gasCommitted; + } + } + + function createAndStoreTask( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint64 _priority, + LibCommon.TaskType _taskType, + uint64 _regTime, + bool _isUst, + bytes[] memory _auxData + ) private returns (uint64 taskIndex) { + RegistryState storage registryState = LibAppStorage.registryState(); + + taskIndex = registryState.currentIndex; + + uint64 taskPriority; + if (_isUst) { + taskPriority = taskIndex; + } else { + taskPriority = _priority == 0 ? taskIndex : _priority; + } + + TaskMetadata memory taskMetadata = TaskMetadata({ + maxGasAmount: _maxGasAmount, + gasPriceCap: _gasPriceCap, + automationFeeCapForCycle: _automationFeeCapForCycle, + depositFee: _automationFeeCapForCycle, + txHash: readTxHash(), + taskIndex: taskIndex, + registrationTime: _regTime, + expiryTime: _expiryTime, + priority: taskPriority, + owner: msg.sender, + taskType: _taskType, + taskState: LibCommon.TaskState.PENDING, + payloadTx: _payloadTx, + predicate: _predicate, + auxData: _auxData + }); + + registryState.tasks[taskIndex] = taskMetadata; + require(registryState.taskIdList.add(taskIndex), IRegistryFacet.TaskIndexNotUnique()); + // taskIndex is strictly monotonically increasing (registryState.currentIndex += 1 below), + // so a plain append here keeps orderedTaskIds ascending by construction — see its + // declaration in LibAppStorage.sol and LibCore.buildAliveOrderedTaskIds. + registryState.orderedTaskIds.push(taskIndex); + require(registryState.addressToTasks[msg.sender].add(taskIndex), IRegistryFacet.TaskIndexNotUnique()); + + if (!_isUst) { + require(registryState.sysTaskIds.add(taskIndex), IRegistryFacet.TaskIndexNotUnique()); + } + registryState.currentIndex += 1; + } + + function reduceGasCommittedForNextCycle(bool _isGst, uint128 _maxGasAmount) private { + RegistryState storage registryState = LibAppStorage.registryState(); + + uint128 gasCommittedForNextCycle = _isGst ? registryState.sysGasCommittedForNextCycle : registryState.gasCommittedForNextCycle; + if (gasCommittedForNextCycle < _maxGasAmount) { revert IRegistryFacet.GasCommittedValueUnderflow(); } + + // Adjust the gas committed for the next cycle by subtracting the gas amount of the cancelled/stopped task + if (_isGst) { + registryState.sysGasCommittedForNextCycle = gasCommittedForNextCycle - _maxGasAmount; + } else { + registryState.gasCommittedForNextCycle = gasCommittedForNextCycle - _maxGasAmount; + + } + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: INTERNAL FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + function registerTask( + bytes memory _payloadTx, + bytes memory _predicate, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint64 _priority, + LibCommon.TaskType _taskType, + bytes[] memory _auxData + ) internal returns (uint64 taskIndex) { + RegistryState storage registryState = LibAppStorage.registryState(); + + uint64 regTime = uint64(block.timestamp); + bool isUst = _taskType == LibCommon.TaskType.UST; + uint256 systemTaskCount = registryState.sysTaskIds.length(); + uint256 registeredTasksCount = registryState.taskIdList.length(); + require(systemTaskCount <= registeredTasksCount, IRegistryFacet.InvalidRegistryState()); + uint256 totalTasks = isUst ? registeredTasksCount - systemTaskCount : systemTaskCount; + + validateAuxData(_auxData); + updateStateForValidRegistration( + totalTasks, + regTime, + _expiryTime, + _payloadTx, + _predicate, + _maxGasAmount, + _gasPriceCap, + _automationFeeCapForCycle, + isUst + ); + + taskIndex = createAndStoreTask( + _payloadTx, + _predicate, + _expiryTime, + _maxGasAmount, + _gasPriceCap, + _automationFeeCapForCycle, + _priority, + _taskType, + regTime, + isUst, + _auxData + ); + } + + function cancelTask( + uint64 _taskIndex, + bool _isGst + ) internal returns (LibCommon.TaskCancelled memory cancelledTask) { + RegistryState storage registryState = LibAppStorage.registryState(); + + TaskMetadataLW memory task = LibCommon.toLW(registryState.tasks[_taskIndex]); + + validateOwnerType(task.owner, task.taskType, _isGst); + if (task.taskState == LibCommon.TaskState.CANCELLED) { revert IRegistryFacet.AlreadyCancelled(); } + if (task.taskState == LibCommon.TaskState.PENDING) { + LibCommon.removeTask(_taskIndex, task.owner, _isGst, false); + + // Refund only for UST + // When Pending tasks are cancelled, refund of the deposit fee is done with penalty + if (!_isGst) { + bool result = LibAccounting.safeDepositRefund( + _taskIndex, + task.owner, + task.depositFee / LibAccounting.REFUND_FACTOR, + task.depositFee + ); + if (!result) revert IRegistryFacet.ErrorDepositRefund(); + } + } else { + // It is safe not to check the state as above, the cancelled tasks are already rejected. + // Active tasks will be refunded the deposited amount fully at the end of the cycle. + registryState.tasks[_taskIndex].taskState = LibCommon.TaskState.CANCELLED; + } + + // This check means the task was expected to be executed in the next cycle, but it has been cancelled. + // We need to remove its gas commitment from `gasCommittedForNextCycle` for this particular task. + if (task.expiryTime > LibCommon.getCycleEndTime()) { + reduceGasCommittedForNextCycle(_isGst, task.maxGasAmount); + } + + cancelledTask = LibCommon.TaskCancelled(_taskIndex, task.taskType, task.txHash); + } + + function stopTask( + uint64 _taskId, + uint64 _cycleEndTime, + uint64 _currentTime, + uint64 _residualInterval, + bool _isGst + ) internal returns (LibCommon.TaskStopped memory taskStopped, uint128 refund) { + RegistryState storage registryState = LibAppStorage.registryState(); + TaskMetadataLW memory task = LibCommon.toLW(registryState.tasks[_taskId]); + + validateOwnerType(task.owner, task.taskType, _isGst); + + (uint128 cycleFeeRefund, uint128 depositRefund) = removeTaskAndComputeRefund( + _taskId, + _cycleEndTime, + _currentTime, + _residualInterval, + task.expiryTime, + task.maxGasAmount, + task.depositFee, + task.owner, + task.taskState, + _isGst + ); + + refund = cycleFeeRefund + depositRefund; + taskStopped = LibCommon.TaskStopped(_taskId, depositRefund, cycleFeeRefund, task.txHash); + } + + function removeTaskAndComputeRefund( + uint64 _taskId, + uint64 _cycleEndTime, + uint64 _currentTime, + uint64 _residualInterval, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _depositFee, + address _owner, + LibCommon.TaskState _taskState, + bool _isGst + ) internal returns (uint128 cycleFeeRefund, uint128 depositRefund) { + // Remove task from the registry and active tasks + LibCommon.removeTask(_taskId, _owner, _isGst, _taskState != LibCommon.TaskState.PENDING); + + // This check means the task was expected to be executed in the next cycle, but it has been stopped. + // We need to remove its gas commitment from `gasCommittedForNextCycle` for this particular task. + // Also it checks that task should not be cancelled. + if (_taskState != LibCommon.TaskState.CANCELLED && _expiryTime > _cycleEndTime) { + // Reduce committed gas by the stopped task's max gas + reduceGasCommittedForNextCycle(_isGst, _maxGasAmount); + } + + if (!_isGst) { + (cycleFeeRefund, depositRefund) = LibAccounting.unlockDepositAndCycleFee( + _taskId, + _taskState, + _expiryTime, + _maxGasAmount, + _residualInterval, + _currentTime, + _depositFee + ); + } + } +} diff --git a/solidity/supra_contracts/src/libraries/LibUtils.sol b/solidity/supra_contracts/src/libraries/LibUtils.sol new file mode 100644 index 0000000000..dce46689c6 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibUtils.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +// Helper library used by Supra contracts +library LibUtils { + + // Custom errors + error AddressCannotBeEOA(); + error AddressCannotBeZero(); + error CallerNotVmSigner(); + + // Address of the VM Signer: SUP0 + address constant VM_SIGNER = address(0x53555000); + + /// @dev Returns a boolean indicating whether the given address is a contract or not. + /// @param _addr The address to be checked. + /// @return A boolean indicating whether the given address is a contract or not. + function isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(_addr) + } + return size > 0; + } + + /// @notice Validates a contract address. + function validateContractAddress(address _contractAddr) internal view { + if (_contractAddr == address(0)) { revert AddressCannotBeZero(); } + if (!isContract(_contractAddr)) { revert AddressCannotBeEOA(); } + } + + /// @notice Validates an address. + function validateAddress(address _addr) internal pure { + if (_addr == address(0)) { revert AddressCannotBeZero(); } + } + + /// @notice Checks if an address is VM Signer, reverts if it is not. + /// @param _addr Address to check. + function enforceIsVmSigner(address _addr) internal pure { + if (_addr != VM_SIGNER) revert CallerNotVmSigner(); + } + + /// @notice Checks if an address is a reserved address. + /// @param _addr Address to check. + /// @return bool If it is a reserved address. + function isReservedAddress(address _addr) internal pure returns (bool) { + uint160 addr = uint160(_addr); + return addr >= uint160(VM_SIGNER) && addr <= uint160(0x535550FF); + } + + /// @notice Converts a uint256 storage array to a uint64 memory array. + /// @param arr The storage array to convert. + /// @return result The values as a uint64 array. + function uint256ArrayToUint64Array(uint256[] storage arr) internal view returns (uint64[] memory result) { + uint256 length = arr.length; + result = new uint64[](length); + for (uint256 i = 0; i < length; i++) { + result[i] = uint64(arr[i]); + } + } +} diff --git a/solidity/supra_contracts/src/upgradeInitializers/DiamondInit.sol b/solidity/supra_contracts/src/upgradeInitializers/DiamondInit.sol new file mode 100644 index 0000000000..b550ea48b4 --- /dev/null +++ b/solidity/supra_contracts/src/upgradeInitializers/DiamondInit.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/******************************************************************************\ +* Credits: Nick Mudge (https://twitter.com/mudgen) +* EIP-2535 Diamonds: https://eips.ethereum.org/EIPS/eip-2535 +* +* Implementation of a diamond. +/******************************************************************************/ + +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { IDiamondLoupe } from "../interfaces/IDiamondLoupe.sol"; +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; +import { IERC173 } from "../interfaces/IERC173.sol"; +import { IERC165 } from "../interfaces/IERC165.sol"; +import { IRegistryStatus } from "../interfaces/IRegistryStatus.sol"; + +import { AppStorage, Config, LibAppStorage, RegistryState} from "../libraries/LibAppStorage.sol"; +import { LibCommon } from "../libraries/LibCommon.sol"; +import { LibUtils } from "../libraries/LibUtils.sol"; +import { InitParams } from "../libraries/DiamondTypes.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +/// @title DiamondInit +/// @notice Initialization contract for the Automation Registry +/// @dev +/// EIP-2535 specifies that the `diamondCut` function takes two optional +/// arguments: address _init and bytes calldata _calldata +/// These arguments are used to execute an arbitrary function using delegatecall +/// in order to set state variables in the diamond during deployment or an upgrade +/// More info here: https://eips.ethereum.org/EIPS/eip-2535#diamond-interface +/// +/// - This contract is NOT a facet and MUST NOT be added to the Diamond. +/// - The `init` function selector is never registered and is therefore +/// not callable through the Diamond after deployment. +/// - `init` runs via delegatecall from `LibDiamond.diamondCut`, executing in the +/// Diamond's own storage, and is `initializer`-guarded so it runs at most once +/// per Diamond (smr-moonshot#3451). +/// +/// This initializer performs the following actions: +/// - Registers supported interfaces for ERC-165, IDiamondCut, IDiamondLoupe, ERC-173, +/// IRegistryStatus. +/// - Sets the active registry configuration, protocol feature flags and trusted addresses. +/// - Establishes initial automation cycle state, index, and timestamp. +/// +/// Later versions of DiamondInit re-initializing the state must use `reinitializer(N)` function tag +/// to have a successful outcome. +contract DiamondInit is Initializable { + + /// @notice Initializes Automation Registry state in Diamond storage + /// @param _params Initialization parameters for the Automation Registry. + /// @param _erc20Supra Address of the ERC20Supra contract. + function init( + InitParams memory _params, + address _erc20Supra + ) external initializer { + AppStorage storage s = LibAppStorage.appStorage(); + + // Adding ERC165 data. Registered once, at genesis, for the facet set present then; + // not reconciled by later diamondCut calls — a facet added, replaced or removed + // post-genesis does not update this mapping. + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + ds.supportedInterfaces[type(IERC165).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondCut).interfaceId] = true; + ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] = true; + ds.supportedInterfaces[type(IERC173).interfaceId] = true; + ds.supportedInterfaces[type(IRegistryStatus).interfaceId] = true; + + + LibCommon.validateConfigParameters( + _params.taskDurationCapSecs, + _params.registryMaxGasCap, + _params.congestionThresholdPercentage, + _params.congestionExponent, + _params.maxCongestionExponent, + _params.taskCapacity, + _params.cycleDurationSecs, + _params.sysTaskDurationCapSecs, + _params.sysRegistryMaxGasCap, + _params.sysTaskCapacity + ); + LibUtils.validateContractAddress(_erc20Supra); + + // --------------------------------------------------------------------- + // Config initialization + // --------------------------------------------------------------------- + Config memory activeConfig = Config({ + registryMaxGasCap: _params.registryMaxGasCap, + sysRegistryMaxGasCap: _params.sysRegistryMaxGasCap, + automationBaseFeeWeiPerSec: _params.automationBaseFeeWeiPerSec, + flatRegistrationFeeWei: _params.flatRegistrationFeeWei, + congestionBaseFeeWeiPerSec: _params.congestionBaseFeeWeiPerSec, + taskDurationCapSecs: _params.taskDurationCapSecs, + sysTaskDurationCapSecs: _params.sysTaskDurationCapSecs, + cycleDurationSecs: _params.cycleDurationSecs, + taskCapacity: _params.taskCapacity, + sysTaskCapacity: _params.sysTaskCapacity, + congestionThresholdPercentage: _params.congestionThresholdPercentage, + congestionExponent: _params.congestionExponent, + maxCongestionExponent: _params.maxCongestionExponent + }); + + s.configuration[LibAppStorage.ACTIVE_CONFIG] = activeConfig; + + s.automationEnabled = _params.automationEnabled; + s.registrationEnabled = _params.registrationEnabled; + s.erc20Supra = _erc20Supra; + + // Default task-registration input size caps. Generous relative to real CALL-only + // payloads (CREATE payloads are not supported) — see ConfigFacet.updateDataLengthCaps + // for the owner-only path to raise them later without a contract upgrade. + s.maxPayloadLength = 4096; + s.maxPredicateLength = 2048; + s.maxAuxDataLength = 0; + s.maxAuxDataEntries = 0; + + // --------------------------------------------------------------------- + // Cycle initialization + // --------------------------------------------------------------------- + ( + LibCommon.CycleState cycleState, + uint64 cycleIndex + ) = _params.automationEnabled + ? (LibCommon.CycleState.STARTED, 1) + : (LibCommon.CycleState.READY, 0); + + s.index = cycleIndex; + s.startTime = uint64(block.timestamp); + s.durationSecs = _params.cycleDurationSecs; + s.cycleState = cycleState; + + // --------------------------------------------------------------------- + // Registry state initialization + // --------------------------------------------------------------------- + RegistryState storage registryState = LibAppStorage.registryState(); + registryState.nextCycleRegistryMaxGasCap = _params.registryMaxGasCap; + registryState.nextCycleSysRegistryMaxGasCap = _params.sysRegistryMaxGasCap; + + // Set last, after every other write above has succeeded: this is the single flag + // DiamondLoupeFacet.isInitialized() (IRegistryStatus) reads. It can never be set back + // to false -- the `initializer` modifier above makes this whole function unrunnable a + // second time -- so once true it stays true for the life of this Diamond. + ds.initialized = true; + } + +} diff --git a/solidity/supra_contracts/submit_governance_action.sh b/solidity/supra_contracts/submit_governance_action.sh new file mode 100644 index 0000000000..a35f5e68d9 --- /dev/null +++ b/solidity/supra_contracts/submit_governance_action.sh @@ -0,0 +1,57 @@ +#!/bin/bash -x + +# Script targeting localnet to initialize cycle monitoring for each block +# by registering AutomationController::monitor_cycle_end entry in block-metadata contract +# Steps: +# - For localnet run: +# - Start a supra localnet chain +# - cp Logs/owners/evm* into env_setup directory created next to this script +# +# - prepare .env file next to script with the following content +# +# MULTISIG_WALLET_ADDRESS=0x0a3fa0df1f4e8777ea4a752a5a06681af6acba49 +# BLOCK_METADATA_ADDRESS=0x2cd6f3c0f0ca46ea1616adf9e396ee99c24559df +# AUTOMATION_CONTROLLER=0x31fb454ab230303b7095064d385cae8d4da4651b +# TIMEOUT=360 +# +# - export PASSWORD variable, otherwise password will be requested during run +# - with value of the CLI_PROFILE_PASSWORD of the local nodes +# +# - run this script +# + +if [ -z "$1" ]; then + echo "Usage: $0 GOV_ACTION_SCRIPT_NAME" +fi + +action=$1 + +password="" +if [ -n ${PASSWORD} ]; then + password="--password ${PASSWORD}" +fi + +script_path=$(dirname $(realpath ${0})) +foundation_owners=( $(ls ${script_path}/env_setup/evm*) ) +foundation_owners_addresses=() +for owner in ${foundation_owners[*]} +do + foundation_owners_addresses+=( $(basename ${owner} | cut -d "_" -f2) ) +done + +echo ${foundation_owners[*]} ${foundation_owners_addresses[*]} + +result=$(forge script ${script_path}/script/GovActions.s.sol:${action} --keystore ${foundation_owners[0]} --sender ${foundation_owners_addresses[0]} --broadcast ${password}) +export GOV_TXN_INDEX=$(echo ${result} | grep -o "TxnIndex: [0-9]* "| cut -d ":" -f2 | tr -d " ") + +echo "Voting for: ${GOV_TXN_INDEX}" +length=${#foundation_owners[@]} +for (( i = 1; i < length; i++ )); do + keystore=${foundation_owners[$i]} + address=${foundation_owners_addresses[$i]} + echo ${keystore} ${address} + forge script ${script_path}/script/GovActions.s.sol:VoteForTxn --keystore ${keystore} --sender ${address} --broadcast ${password} +done + +echo "Executing Txn with index: ${GOV_TXN_INDEX}" +forge script ${script_path}/script/GovActions.s.sol:ExecuteTxn --keystore ${foundation_owners[0]} --sender ${foundation_owners_addresses[0]} --broadcast ${password} diff --git a/solidity/supra_contracts/test/AutomationFeeMultiplier.t.sol b/solidity/supra_contracts/test/AutomationFeeMultiplier.t.sol new file mode 100644 index 0000000000..852d0bd7a7 --- /dev/null +++ b/solidity/supra_contracts/test/AutomationFeeMultiplier.t.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test, stdError} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {InitParams} from "../src/libraries/DiamondTypes.sol"; +import {Deployment, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; + +/// @notice Tests for calculateAutomationFeeMultiplierForCommittedOccupancy. +/// +/// Configuration under test (exactly as specified by the user): +/// registryMaxGasCap = 300_000_000 (= 300 * 1_000_000) +/// congestionThresholdPct = 75 (75 %) +/// congestionExponent = 6 +/// congestionBaseFeeWeiPerSec = 1_714_530_600_000 +/// automationBaseFeeWeiPerSec = 1_714_530_600_000 (= CONGESTION_BASE_FEE) +/// +/// ─── Formula (LibAccounting.calculateAutomationCongestionFee) ──────────────── +/// +/// DECIMAL = 1e8 +/// thresholdUsageScaled = (totalGas * DECIMAL * 100) / maxGas +/// thresholdPctScaled = threshold * DECIMAL +/// +/// if usage <= thresholdPctScaled → congestion fee = 0 (short-circuit) +/// if congestionBaseFee == 0 → congestion fee = 0 (short-circuit) +/// if threshold == 100 → congestion fee = 0 (short-circuit) +/// +/// surplus = min(usage, 100*DECIMAL) − thresholdPctScaled +/// surplusScaled = surplus / 100 +/// expResult = ((1 + surplusScaled) ^ exponent) − 1 [fixed-point, DECIMAL=1e8] +/// congestionFee = (congestionBaseFee * expResult) / DECIMAL +/// +/// totalMultiplier = congestionFee + automationBaseFeeWeiPerSec +/// +/// ─── Derivation for the 100 % occupancy case ───────────────────────────────── +/// +/// thresholdUsageScaled = (300_000_000 × 1e8 × 100) / 300_000_000 = 1e10 +/// thresholdPctScaled = 75 × 1e8 = 7_500_000_000 +/// surplus = 1e10 − 7.5e9 = 2_500_000_000 +/// surplusScaled = 25_000_000 (= 0.25 in fixed-point) +/// +/// calculateExponentiation(25_000_000, 6): +/// base = 1e8 + 25_000_000 = 125_000_000 (represents 1.25) +/// result = 1e8 +/// +/// iter 1 (exponent=6, bit=0): skip; base = (1.25²)×1e8 = 156_250_000 +/// iter 2 (exponent=3, bit=1): result = 156_250_000; +/// base = (1.5625²)×1e8 = 244_140_625 +/// iter 3 (exponent=1, bit=1): result = (156_250_000 × 244_140_625) / 1e8 +/// = 38_146_972_656_250_000 / 1e8 +/// = 381_469_726 (floor; exact: 381_469_726.5625) +/// return 381_469_726 − 1e8 = 281_469_726 +/// (exact: 1.25^6 − 1 = 2.814697265625 → ×1e8 = 281_469_726.5625 → truncated) +/// +/// congestionFee = (1_714_530_600_000 × 281_469_726) / 1e8 +/// = 482_588_458_200_615_600_000 / 1e8 +/// = 4_825_884_582_006 (floor; remainder 15_600_000) +/// totalMultiplier = 4_825_884_582_006 + 1_714_530_600_000 = 6_540_415_182_006 +/// +/// ─── Derivation for the 76 % occupancy case ────────────────────────────────── +/// +/// thresholdUsageScaled = 76 × 1e8 = 7_600_000_000 +/// surplus = 100_000_000 ; surplusScaled = 1_000_000 (= 0.01) +/// +/// calculateExponentiation(1_000_000, 6): +/// base = 101_000_000 +/// iter 1 (bit=0): base = (1.01²)×1e8 = 102_010_000 +/// iter 2 (bit=1): result = 102_010_000; +/// base = (102_010_000²)/1e8 = 104_060_401 +/// iter 3 (bit=1): result = (102_010_000 × 104_060_401) / 1e8 +/// = 10_615_201_506_010_000 / 1e8 +/// = 106_152_015 (floor) +/// return 6_152_015 (exact: 1.01^6−1 ≈ 0.06152015, ×1e8 = 6_152_015.06…) +/// +/// congestionFee = (1_714_530_600_000 × 6_152_015) / 1e8 +/// = 10_547_817_969_159_000_000 / 1e8 +/// = 105_478_179_691 (floor; remainder 59_000_000) +/// totalMultiplier = 105_478_179_691 + 1_714_530_600_000 = 1_820_008_779_691 +contract AutomationFeeMultiplierTest is Test { + + // ── User-specified config ────────────────────────────────────────────── + uint128 constant REGISTRY_MAX_GAS = 300_000_000; // 300 * 1_000_000 + uint8 constant CONGESTION_THRESHOLD = 75; + uint8 constant CONGESTION_EXPONENT = 6; + uint128 constant CONGESTION_BASE_FEE = 1_714_530_600_000; // derived from 0.00000000000017145306 ETH/s at 18 decimals + uint128 constant AUTOMATION_BASE_FEE = 1_714_530_600_000; // = CONGESTION_BASE_FEE + + // ── Pre-computed expected values (congestionFee + AUTOMATION_BASE_FEE; see file header) ── + uint128 constant EXPECTED_FEE_100_PCT = 6_540_415_182_006; // 4_825_884_582_006 + 1_714_530_600_000 + uint128 constant EXPECTED_FEE_76_PCT = 1_820_008_779_691; // 105_478_179_691 + 1_714_530_600_000 + + // ── Addresses / contracts ────────────────────────────────────────────── + address admin = address(0xA11CE); + address bridge = address(0xBEEF); + ERC20Supra erc20Supra; + address testDiamond; + + // ── TX-hash precompile required by BaseDiamondTest infra ────────────── + address constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + // ══════════════════════════════════════════════════════════════════════════ + // Setup + // ══════════════════════════════════════════════════════════════════════════ + + function setUp() public { + // Mock the TX-hash precompile so Diamond deployment doesn't revert. + vm.mockCall( + TX_HASH_PRECOMPILE, + bytes(""), + abi.encode(keccak256("txHash")) + ); + + // Deploy ERC20Supra (the Diamond requires a valid contract address). + vm.startPrank(admin); + address[] memory authorized = new address[](1); + authorized[0] = bridge; + ERC20Supra impl = new ERC20Supra(); + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (admin, authorized)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + erc20Supra = ERC20Supra(address(proxy)); + vm.stopPrank(); + + // Deploy the diamond with the user-specified config. + testDiamond = _deployDiamond(AUTOMATION_BASE_FEE); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Helpers + // ══════════════════════════════════════════════════════════════════════════ + + /// @dev Deploys a fresh diamond whose active config contains the user-specified + /// congestion parameters plus the provided automationBaseFeeWeiPerSec. + function _deployDiamond(uint128 automationBaseFee) internal returns (address) { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.registryMaxGasCap = REGISTRY_MAX_GAS; + p.sysRegistryMaxGasCap = REGISTRY_MAX_GAS; + p.congestionThresholdPercentage = CONGESTION_THRESHOLD; + p.congestionExponent = CONGESTION_EXPONENT; + p.congestionBaseFeeWeiPerSec = CONGESTION_BASE_FEE; + p.automationBaseFeeWeiPerSec = automationBaseFee; + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + vm.stopPrank(); + return d.diamond; + } + + /// @dev Wraps the function under test. + function _calc(address diamond, uint128 committedGas) internal view returns (uint128) { + return IRegistryFacet(diamond) + .calculateAutomationFeeMultiplierForCommittedOccupancy(committedGas); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Tests + // ══════════════════════════════════════════════════════════════════════════ + + // ── Zero occupancy ──────────────────────────────────────────────────────── + + /// @dev 0 % usage is far below the 75 % threshold → no congestion fee. + /// Only automationBaseFee is returned (1_714_530_600_000). + function testFeeMultiplier_ZeroOccupancy() public view { + assertEq(_calc(testDiamond, 0), AUTOMATION_BASE_FEE, + "zero occupancy: multiplier must equal automation base fee only"); + } + + // ── Below threshold: 74 % ───────────────────────────────────────────────── + + /// @dev 74 % usage (222_000_000) is strictly below the 75 % threshold. + /// thresholdUsageScaled = 7_400_000_000 ≤ 7_500_000_000 → no congestion. + function testFeeMultiplier_BelowThreshold_74Pct() public view { + // Use integer arithmetic to avoid any rounding artefact in the input. + uint128 gas74 = uint128((74 * uint256(REGISTRY_MAX_GAS)) / 100); // 222_000_000 + assertEq(_calc(testDiamond, gas74), AUTOMATION_BASE_FEE, + "74% occupancy: only automation base fee (no congestion)"); + } + + // ── Exactly at threshold: 75 % ──────────────────────────────────────────── + + /// @dev The guard condition is `≤`, so at exactly the threshold value the + /// congestion fee is still zero. + /// thresholdUsageScaled = 7_500_000_000 == thresholdPctScaled → no congestion. + function testFeeMultiplier_AtThreshold_75Pct() public view { + uint128 gas75 = uint128((75 * uint256(REGISTRY_MAX_GAS)) / 100); // 225_000_000 + assertEq(_calc(testDiamond, gas75), AUTOMATION_BASE_FEE, + "75% occupancy (at threshold): no congestion fee"); + } + + // ── Just above threshold: 76 % ──────────────────────────────────────────── + + /// @dev 76 % usage triggers a small but non-zero congestion fee. + /// Expected total = congestionFee(76%) + automationBaseFee + /// = 105_478_179_691 + 1_714_530_600_000 = 1_820_008_779_691. + function testFeeMultiplier_JustAboveThreshold_76Pct() public view { + uint128 gas76 = uint128((76 * uint256(REGISTRY_MAX_GAS)) / 100); // 228_000_000 + uint128 result = _calc(testDiamond, gas76); + + assertEq(result, EXPECTED_FEE_76_PCT, + "76% occupancy: unexpected congestion fee value"); + assertGt(result, 0, + "76% occupancy: fee must be strictly positive"); + assertLt(result, EXPECTED_FEE_100_PCT, + "76% occupancy: fee must be less than 100% fee"); + } + + // ── Primary case: 100 % occupancy (totalCommittedMaxGas = registryMaxGas) ── + + /// @dev Full-registry occupancy with the exact user-supplied parameters. + /// Expected total = congestionFee(100%) + automationBaseFee + /// = 4_825_884_582_006 + 1_714_530_600_000 = 6_540_415_182_006. + function testFeeMultiplier_FullOccupancy_100Pct() public view { + // totalCommittedMaxGas = 300 * 1_000_000 = registryMaxGas + uint128 result = _calc(testDiamond, REGISTRY_MAX_GAS); + assertEq(result, EXPECTED_FEE_100_PCT, + "100% occupancy: unexpected congestion fee value"); + } + + // ── Over-committed: totalGas > registryMaxGas ───────────────────────────── + + /// @dev The surplus is capped at (100 % − threshold) when totalGas > maxGas, + /// so the fee at 200 % commitment equals the fee at 100 % commitment. + /// + /// thresholdUsageScaled at 200% = 20_000_000_000 > 100 * DECIMAL + /// → surplus capped at (1e10 − 7.5e9) = 2_500_000_000 (same as 100%) + function testFeeMultiplier_OverCommitted_200Pct() public view { + uint128 gas200pct = 2 * REGISTRY_MAX_GAS; // 600_000_000 + assertEq(_calc(testDiamond, gas200pct), EXPECTED_FEE_100_PCT, + "200% over-commitment: fee must be capped at the 100% level"); + } + + // ── Monotonicity ────────────────────────────────────────────────────────── + + /// @dev The congestion multiplier must be non-decreasing as committed gas grows + /// (all other config held constant). + function testFeeMultiplier_MonotonicallyNonDecreasing() public view { + uint128 fee74 = _calc(testDiamond, uint128((74 * uint256(REGISTRY_MAX_GAS)) / 100)); + uint128 fee75 = _calc(testDiamond, uint128((75 * uint256(REGISTRY_MAX_GAS)) / 100)); + uint128 fee76 = _calc(testDiamond, uint128((76 * uint256(REGISTRY_MAX_GAS)) / 100)); + uint128 fee90 = _calc(testDiamond, uint128((90 * uint256(REGISTRY_MAX_GAS)) / 100)); + uint128 fee100 = _calc(testDiamond, REGISTRY_MAX_GAS); + + assertLe(fee74, fee75, "74% <= 75%"); + assertLe(fee75, fee76, "75% <= 76%"); + assertLe(fee76, fee90, "76% <= 90%"); + assertLe(fee90, fee100, "90% <= 100%"); + } + + // ── Fee decomposition: baseFee is present at every occupancy level ──────── + + /// @dev Verifies that automationBaseFeeWeiPerSec (= 1_714_530_600_000) is always + /// present in the result regardless of congestion state. + /// + /// Below threshold: result == AUTOMATION_BASE_FEE (no congestion component) + /// Above threshold: result == congestionFee + AUTOMATION_BASE_FEE + /// + /// The gap between any two occupancy levels must equal the gap between their + /// pure congestion fees (the base fee cancels out in the difference). + function testFeeMultiplier_BaseFeeAlwaysPresent() public view { + uint128 gas74 = uint128((74 * uint256(REGISTRY_MAX_GAS)) / 100); + uint128 gas100 = REGISTRY_MAX_GAS; + + uint128 resultBelow = _calc(testDiamond, gas74); + uint128 resultFull = _calc(testDiamond, gas100); + + // Below threshold: only the base fee, no congestion. + assertEq(resultBelow, AUTOMATION_BASE_FEE, + "below threshold: result must equal automation base fee only"); + + // At full occupancy: total = congestionFee(100%) + baseFee = 6_540_415_182_006. + assertEq(resultFull, EXPECTED_FEE_100_PCT, + "100% occupancy: result must equal congestionFee + automationBaseFee"); + + // The difference isolates the pure congestion component (baseFee cancels out). + uint128 congestionOnly = resultFull - resultBelow; // 4_825_884_582_006 + assertEq(congestionOnly, 4_825_884_582_006, + "fee delta must equal the congestion-only component"); + } + + // ── Threshold = 100 % (short-circuit guard) ─────────────────────────────── + + /// @dev When congestionThresholdPercentage = 100 the implementation immediately + /// returns 0 for the congestion portion (guard: `if (threshold == 100) return 0`). + /// Even at full occupancy, only the automation base fee is returned. + function testFeeMultiplier_ThresholdAt100_NoCongestionEver() public { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.registryMaxGasCap = REGISTRY_MAX_GAS; + p.sysRegistryMaxGasCap = REGISTRY_MAX_GAS; + p.congestionThresholdPercentage = 100; // short-circuit active + p.congestionExponent = CONGESTION_EXPONENT; + p.congestionBaseFeeWeiPerSec = CONGESTION_BASE_FEE; + p.automationBaseFeeWeiPerSec = 0; + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + vm.stopPrank(); + + assertEq(_calc(d.diamond, REGISTRY_MAX_GAS), 0, + "threshold=100%: no congestion fee even at full occupancy"); + } + + // ── Zero congestion base fee (congestion pricing disabled) ──────────────── + + /// @dev When congestionBaseFeeWeiPerSec = 0 the implementation immediately + /// returns 0 for the congestion portion (guard: `if (baseFee == 0) return 0`). + function testFeeMultiplier_ZeroCongestionBaseFee_NoCongestionEver() public { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.registryMaxGasCap = REGISTRY_MAX_GAS; + p.sysRegistryMaxGasCap = REGISTRY_MAX_GAS; + p.congestionThresholdPercentage = CONGESTION_THRESHOLD; + p.congestionExponent = CONGESTION_EXPONENT; + p.congestionBaseFeeWeiPerSec = 0; // disabled + p.automationBaseFeeWeiPerSec = 0; + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + vm.stopPrank(); + + assertEq(_calc(d.diamond, REGISTRY_MAX_GAS), 0, + "congestionBaseFee=0: no congestion fee at any occupancy"); + } + + // ── Exponentiation overflow-guard regression ──────────────────────────────────── + // + // Both tests below configure 0% threshold + 100% occupancy to hit the largest + // congestion multiplier base this code path can produce (surplusScaled = 1e8, so + // baseScaled starts at exactly 2*DECIMAL). + // + // At that base, exhaustive simulation of the exact fixed-point arithmetic + // (calculateExponentiation's loop, THEN calculateAutomationCongestionFee's + // subsequent `congestionBaseFeeWeiPerSec * exponentResult` multiplication, + // using this file's CONGESTION_BASE_FEE constant) shows the end-to-end safe + // exponent range is _exponent <= 119 for this specific base fee — the + // multiplication against congestionBaseFeeWeiPerSec can overflow even where + // calculateExponentiation's own result was still representable, so the exact + // boundary is config-dependent, not a fixed property of the exponentiation + // helper alone. + // The actual safety net in practice is the governance-configured + // maxCongestionExponent cap (default 6, see LibDiamondUtils.defaultInitParams + // and LibCommon.validateConfigParameters) — exponents anywhere near this + // boundary are already far outside any sane fee curve. + + /// @dev _exponent = 119 (the exact end-to-end-safe boundary at this base and + /// base fee, found by exhaustive simulation) must succeed and return a + /// sane, nonzero fee. + function testFeeMultiplier_HighExponentAtNewSafeBoundary() public { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.registryMaxGasCap = REGISTRY_MAX_GAS; + p.sysRegistryMaxGasCap = REGISTRY_MAX_GAS; + p.congestionThresholdPercentage = 0; + p.congestionExponent = 119; + p.maxCongestionExponent = 119; + p.congestionBaseFeeWeiPerSec = CONGESTION_BASE_FEE; + p.automationBaseFeeWeiPerSec = AUTOMATION_BASE_FEE; + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + vm.stopPrank(); + + uint128 fee = _calc(d.diamond, REGISTRY_MAX_GAS); + assertGt(fee, AUTOMATION_BASE_FEE, "expected a nonzero congestion component on top of the base fee"); + } + + /// @dev _exponent = type(uint8).max (255) at the same worst-case base is + /// mathematically too large to fit in uint256 regardless of algorithm — + /// 2.0^255 scaled by DECIMAL exceeds type(uint256).max. This must still + /// revert (a safe Panic, not silent corruption); it is documented here so + /// the real defense — bounding congestionExponent via maxCongestionExponent + /// — doesn't get silently regressed under the assumption the squaring + /// guard alone made every uint8 value safe. + function testFeeMultiplier_ExtremeExponentStillOverflows() public { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.registryMaxGasCap = REGISTRY_MAX_GAS; + p.sysRegistryMaxGasCap = REGISTRY_MAX_GAS; + p.congestionThresholdPercentage = 0; + p.congestionExponent = type(uint8).max; + p.maxCongestionExponent = type(uint8).max; + p.congestionBaseFeeWeiPerSec = CONGESTION_BASE_FEE; + p.automationBaseFeeWeiPerSec = AUTOMATION_BASE_FEE; + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + vm.stopPrank(); + + vm.expectRevert(stdError.arithmeticError); + _calc(d.diamond, REGISTRY_MAX_GAS); + } +} diff --git a/solidity/supra_contracts/test/BaseDiamondTest.t.sol b/solidity/supra_contracts/test/BaseDiamondTest.t.sol new file mode 100644 index 0000000000..3716d907b9 --- /dev/null +++ b/solidity/supra_contracts/test/BaseDiamondTest.t.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; + +abstract contract BaseDiamondTest is Test { + ERC20Supra erc20Supra; // ERC20Supra contract + ERC20SupraHandler erc20SupraHandler; // ERC20SupraHandler contract + address diamondAddr; // Diamond address + + InitParams defaultParams; // Default initialization parameters + Deployment deployment; // Struct containing deployed contract addresses + + /// @dev Address of the transaction hash precompile. + address constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + address admin = address(0xA11CE); + address alice = address(0x123); + address bob = address(0x456); + address bridge = address(0x789); + address erc20SupraHandlerAddr; + + /// @dev Sets up initial state for testing. + /// @dev Sets balance of 'alice' to 100 ether. + /// @dev Deploys all the contracts and initializes the Diamond with required parameters. + function setUp() public { + vm.deal(alice, 500 ether); + + erc20SupraHandlerAddr = vm.computeCreateAddress(admin, 3); + erc20Supra = ERC20Supra(deployErc20Supra(bridge, erc20SupraHandlerAddr)); + + vm.startPrank(admin); + ERC20SupraHandler impl = new ERC20SupraHandler(); + bytes memory initData = abi.encodeCall(ERC20SupraHandler.initialize, (admin, address(erc20Supra))); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + erc20SupraHandler = ERC20SupraHandler(payable(address(proxy))); + + defaultParams = LibDiamondUtils.defaultInitParams(); + deployment = LibDiamondUtils.deploy(admin, address(erc20Supra), defaultParams); + diamondAddr = deployment.diamond; + + IConfigFacet(diamondAddr).grantAuthorization(bob); + + vm.stopPrank(); + + vm.mockCall( + TX_HASH_PRECOMPILE, + bytes(""), + abi.encode(keccak256("txHash")) + ); + } + + /// @dev Helper function to deploy ERC20Supra contract. + function deployErc20Supra(address _bridge, address _erc20SupraHandlerAddr) internal returns (address) { + vm.startPrank(admin); + ERC20Supra impl = new ERC20Supra(); + + address[] memory authorizedAddresses = new address[](2); + authorizedAddresses[0] = _bridge; + authorizedAddresses[1] = _erc20SupraHandlerAddr; + + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (admin, authorizedAddresses)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + vm.stopPrank(); + + return address(proxy); + } + + /// @dev Helper function to register a UST. + /// @param _diamond The address of the diamond. + /// @param _duration The duration of the UST. + function registerUst(address _diamond, uint64 _duration) internal { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(_diamond); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(_diamond, type(uint256).max); + + IRegistryFacet(_diamond).register( + payload, // payload + predicate, // predicate + uint64(block.timestamp + _duration), // expiryTime + uint128(100_000), // maxGasAmount + uint128(4 gwei), // gasPriceCap + uint128(60.1 ether), // automationFeeCapForCycle + 2, // priority + auxData // aux data + ); + vm.stopPrank(); + } + + /// @dev Helper function to register a GST. + /// @param _diamond The address of the diamond. + /// @param _duration The duration of the GST. + function registerGst(address _diamond, uint64 _duration) internal { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(_diamond); + + vm.prank(bob); + IRegistryFacet(_diamond).registerSystemTask( + payload, // payload + predicate, // predicate + uint64(block.timestamp + _duration), // expiryTime + uint128(100_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + /// @dev Helper function to return payload. + /// @param _value Value to be sent along with the transaction. + /// @param _target Address of the destination smart contract. + /// @param _callData Calldata to be sent along with the transaction. + function createPayload(uint128 _value, address _target, bytes memory _callData) internal pure returns (bytes memory) { + LibCommon.AccessListEntry[] memory accessList = new LibCommon.AccessListEntry[](2); + + bytes32[] memory keys = new bytes32[](2); + keys[0] = bytes32(uint256(0)); + keys[1] = bytes32(uint256(1)); + + accessList[0] = LibCommon.AccessListEntry({ + addr: address(0x1111), + storageKeys: keys + }); + + accessList[1] = LibCommon.AccessListEntry({ + addr: address(0x2222), + storageKeys: keys + }); + + bytes memory payload = abi.encode(_value, _target, _callData, accessList); + + return payload; + } + + /// @notice Helper function to create a predicate + /// @param _target Address of the contract to call + function createPredicate(address _target) internal pure returns (bytes memory) { + // Creates a predicate that checks if registration is enabled + bytes memory callData = abi.encodeCall(IConfigFacet.isRegistrationEnabled, ()); + return abi.encode(_target, callData); + } + + /// @dev Helper to warp past the current cycle, end it, and process the given tasks. + function processCycleTransition(address _diamond, uint256[] memory _taskIndexes) internal { + (uint64 indexBefore, uint64 startTimeBefore, uint64 durationBefore, ) = ICoreFacet(_diamond).getCycleInfo(); + vm.warp(startTimeBefore + durationBefore); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(_diamond).monitorCycleEnd(); + + (uint64 indexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(_diamond).getCycleInfo(); + assertEq(indexAfter, indexBefore); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.FINISHED)); + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.ActiveTasks(_taskIndexes); + + ICoreFacet(_diamond).processTasks(indexBefore + 1, _taskIndexes); + vm.stopPrank(); + } + + /// @dev Helper function to deploy a custom AutomationRegistry with: + /// - taskCapacity and sysTaskCapacity set to 2 + /// - automation and congestion base fees set to 0 + function deployCustomRegistry() internal returns (address) { + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 2, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 2, + registrationEnabled: true, + automationEnabled: true + }); + + vm.startPrank(admin); + Deployment memory customDeployment = LibDiamondUtils.deploy(admin, address(erc20Supra), initParams); + address diamond = customDeployment.diamond; + IConfigFacet(diamond).grantAuthorization(bob); + vm.stopPrank(); + + return diamond; + } +} + +/// @dev Mock ERC20 that returns `false` on transfer/transferFrom to test TransferFailed revert paths. +contract FailingERC20 { + function balanceOf(address) external pure returns (uint256) { return 100 ether; } + function transfer(address, uint256) external pure returns (bool) { return false; } + function transferFrom(address, address, uint256) external pure returns (bool) { return false; } +} diff --git a/solidity/supra_contracts/test/BlockMeta.t.sol b/solidity/supra_contracts/test/BlockMeta.t.sol new file mode 100644 index 0000000000..288da51c78 --- /dev/null +++ b/solidity/supra_contracts/test/BlockMeta.t.sol @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {BlockMeta} from "../src/BlockMeta.sol"; +import {Counter} from "./Counter.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {IBlockMeta} from "../src/interfaces/IBlockMeta.sol"; + +contract BlockMetaTest is Test { + BlockMeta blockMeta; // BlockMeta instance on proxy address + Counter counter; // Counter instance on proxy address + address counterAddress; + bytes4 selector; + + address admin = address(0xA11CE); + address vmAddress = address(0x99); + address alice = address(0x123); + + /// @dev Sets up initial state for testing. + /// @dev Deploys and initializes BlockMeta and AutomationController contracts. + function setUp() public { + vm.startPrank(admin); + + // Deploy BlockMeta proxy + BlockMeta blockMetaImpl = new BlockMeta(); + bytes memory blockMetaInitData = abi.encodeCall(BlockMeta.initialize, (admin, 1_000_000)); + ERC1967Proxy blockMetaProxy = new ERC1967Proxy(address(blockMetaImpl), blockMetaInitData); + blockMeta = BlockMeta(address(blockMetaProxy)); + + Counter counterImpl = new Counter(); + bytes memory counterInitData = abi.encodeCall(Counter.initialize, (address(blockMeta))); + ERC1967Proxy counterProxy = new ERC1967Proxy(address(counterImpl), counterInitData); + counter = Counter(address(counterProxy)); + + counterAddress = address(counter); + selector = Counter.increment.selector; + + vm.stopPrank(); + } + + uint64 constant DEFAULT_GAS = 50_000; + + /// @dev Helper function to register a selector with a default gas limit. + /// @param _targetContract The target contract address. + /// @param _selector Function selector to register. + function register(address _targetContract, bytes4 _selector) private { + register(_targetContract, _selector, DEFAULT_GAS); + } + + function register(address _targetContract, bytes4 _selector, uint64 _gasLimit) private { + vm.prank(admin); + blockMeta.register(_targetContract, _selector, _gasLimit); + } + + /// @dev Test to ensure 'register' registers a selector. + function testRegister() public { + address[] memory targets; + bytes4[] memory selectors; + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 0); + assertEq(selectors.length, 0); + assertEq(blockMeta.totalGasAllocated(), 0); + + register(counterAddress, selector); + + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 1); + assertEq(selectors.length, 1); + assertEq(targets[0], counterAddress); + assertEq(selectors[0], selector); + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS); + } + + /// @dev Test to ensure 'register' emits event 'SelectorRegistered'. + function testRegisterEmitsEvent() public { + vm.expectEmit(true, true, true, false); + emit IBlockMeta.SelectorRegistered(counterAddress, selector, DEFAULT_GAS); + + register(counterAddress, selector); + } + + /// @dev Test to ensure 'register' reverts if caller is not owner. + function testRegisterRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + blockMeta.register(counterAddress, selector, DEFAULT_GAS); + } + + /// @dev Test to ensure 'register' reverts if address(0) is passed. + function testRegisterRevertsIfAddressZero() public { + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + + register(address(0), selector); + } + + /// @dev Test to ensure 'register' reverts if EOA is passed. + function testRegisterRevertsIfEOA() public { + vm.expectRevert(LibUtils.AddressCannotBeEOA.selector); + + register(alice, selector); + } + + /// @dev Test to ensure 'register' reverts if empty selector is passed. + function testRegisterRevertsIfEmptySelector() public { + vm.expectRevert(IBlockMeta.InvalidSelector.selector); + + register(counterAddress, bytes4(0)); + } + + /// @dev Test to ensure 'register' reverts if gas limit is zero. + function testRegisterRevertsIfGasLimitZero() public { + vm.expectRevert(IBlockMeta.InvalidGasLimit.selector); + register(counterAddress, selector, 0); + } + + /// @dev Test to ensure 'register' reverts if total allocated gas exceeds the cap. + function testRegisterRevertsIfGasCapExceeded() public { + register(counterAddress, selector, 800_000); + + vm.expectRevert(IBlockMeta.GasCapExceeded.selector); + register(counterAddress, bytes4(keccak256("foo()")), 200_001); + } + + /// @dev Test to ensure 'register' accepts a total exactly at the 63/64 forwarding-rule + /// bound of 'blockPrologueGasCap' (1_000_000 set in setUp -> bound is exactly 984_375). + function testRegisterAtExact63_64BoundarySucceeds() public { + register(counterAddress, selector, 984_375); + assertEq(blockMeta.totalGasAllocated(), 984_375); + } + + /// @dev Test to ensure 'register' reverts one gas unit above the 63/64 forwarding-rule + /// bound, even though it is still within the raw 'blockPrologueGasCap' of 1_000_000. + function testRegisterOneGasAbove63_64BoundaryReverts() public { + vm.expectRevert(IBlockMeta.GasCapExceeded.selector); + register(counterAddress, selector, 984_376); + } + + /// @dev Test to ensure 'register' reverts if selector already exists. + function testRegisterRevertsIfSelectorAlreadyExists() public { + testRegister(); + + vm.expectRevert(IBlockMeta.SelectorAlreadyRegistered.selector); + register(counterAddress, selector); + } + + /// @dev Test to ensure 'deregister' deregisters a selector. + function testDeregister() public { + bytes4 foo = bytes4(keccak256("foo()")); + register(counterAddress, selector); + register(counterAddress, foo); + + address[] memory targets; + bytes4[] memory selectors; + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 2); + assertEq(selectors.length, 2); + assertEq(targets[0], counterAddress); + assertEq(targets[1], counterAddress); + assertEq(selectors[0], selector); + assertEq(selectors[1], foo); + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS * 2); + + vm.prank(admin); + blockMeta.deregister(counterAddress, selector); + + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 1); + assertEq(selectors.length, 1); + assertEq(targets[0], counterAddress); + assertEq(selectors[0], foo); + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS); + } + + /// @dev Test to ensure 'deregister' emits event 'SelectorDeregistered'. + function testDeregisterEmitsEvent() public { + testRegister(); + + vm.expectEmit(true, true, true, false); + emit IBlockMeta.SelectorDeregistered(counterAddress, selector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.deregister(counterAddress, selector); + } + + /// @dev Test to ensure 'deregister' reverts if caller is not owner. + function testDeregisterRevertsIfNotOwner() public { + testRegister(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + blockMeta.deregister(counterAddress, selector); + } + + /// @dev Test to ensure 'deregister' reverts if selector does not exist. + function testDeregisterRevertsIfSelectorDoesNotExist() public { + testRegister(); + + bytes4 invalidSelector = bytes4(keccak256("foo()")); + + vm.expectRevert(IBlockMeta.SelectorNotRegistered.selector); + + vm.prank(admin); + blockMeta.deregister(counterAddress, invalidSelector); + } + + /// @dev Test to ensure 'deregisterAt' deregisters a selector at an index. + function testDeregisterAt() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + register(counterAddress, selector); + register(address(failingContract), failSelector); + + address[] memory targets; + bytes4[] memory selectors; + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 2); + assertEq(selectors.length, 2); + assertEq(targets[0], counterAddress); + assertEq(targets[1], address(failingContract)); + assertEq(selectors[0], selector); + assertEq(selectors[1], failSelector); + + vm.prank(admin); + blockMeta.deregisterAt(0); + + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 1); + assertEq(selectors.length, 1); + assertEq(targets[0], address(failingContract)); + assertEq(selectors[0], failSelector); + } + + /// @dev Test to ensure 'deregisterAt' emits event 'SelectorDeregistered'. + function testDeregisterAtEmitsEvent() public { + testRegister(); + + vm.expectEmit(true, true, true, false); + emit IBlockMeta.SelectorDeregistered(counterAddress, selector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.deregisterAt(0); + } + + /// @dev Test to ensure 'deregisterAt' reverts if caller is not owner. + function testDeregisterAtRevertsIfNotOwner() public { + testRegister(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + blockMeta.deregisterAt(0); + } + + /// @dev Test to ensure 'deregisterAt' reverts if invalid index is passed. + function testDeregisterAtRevertsIfInvalidIndex() public { + testRegister(); + + vm.expectRevert(IBlockMeta.InvalidIndex.selector); + + vm.prank(admin); + blockMeta.deregisterAt(1); + } + + /// @dev Test to ensure 'updateExecutionOrder' updates the execution order. + function testUpdateExecutionOrder() public { + testRegister(); + + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(address(failingContract), failSelector, DEFAULT_GAS); + executionOrder[1] = packExecution(counterAddress, selector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + + (address[] memory targets, bytes4[] memory selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 2); + assertEq(selectors.length, 2); + assertEq(targets[0], address(failingContract)); + assertEq(targets[1], counterAddress); + assertEq(selectors[0], failSelector); + assertEq(selectors[1], selector); + } + + /// @dev Test to ensure 'updateExecutionOrder' emits event 'ExecutionOrderUpdated'. + function testUpdateExecutionOrderEmitsEvent() public { + testRegister(); + + uint256[] memory executionOrder = createExecutionOrder(); + + vm.expectEmit(true, false, false, false); + emit IBlockMeta.ExecutionOrderUpdated(executionOrder); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if caller is not owner. + function testUpdateExecutionOrderRevertsIfNotOwner() public { + uint256[] memory executionOrder = createExecutionOrder(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if address(0) is passed as target. + function testUpdateExecutionOrderRevertsIfTargetAddressZero() public { + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(counterAddress, selector, DEFAULT_GAS); + executionOrder[1] = packExecution(address(0), selector, DEFAULT_GAS); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if EOA is passed as target. + function testUpdateExecutionOrderRevertsIfTargetAddressEOA() public { + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(counterAddress, selector, DEFAULT_GAS); + executionOrder[1] = packExecution(alice, selector, DEFAULT_GAS); + + vm.expectRevert(LibUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if empty selector is passed + function testUpdateExecutionOrderRevertsIfEmptySelector() public { + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(counterAddress, selector, DEFAULT_GAS); + executionOrder[1] = packExecution(counterAddress, bytes4(0), DEFAULT_GAS); + + vm.expectRevert(IBlockMeta.InvalidSelector.selector); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if zero gas limit is passed. + function testUpdateExecutionOrderRevertsIfGasLimitZero() public { + uint256[] memory executionOrder = new uint256[](1); + executionOrder[0] = packExecution(counterAddress, selector, 0); + + vm.prank(admin); + vm.expectRevert(IBlockMeta.InvalidGasLimit.selector); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if duplicate selector is passed. + function testUpdateExecutionOrderRevertsIfDuplicateSelector() public { + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(counterAddress, selector, DEFAULT_GAS); + executionOrder[1] = packExecution(counterAddress, selector, DEFAULT_GAS); + + vm.expectRevert(IBlockMeta.SelectorAlreadyRegistered.selector); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' decreases execution order length. + function testUpdateExecutionOrderDecreasesExecutionOrder() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(counterAddress, selector); + register(address(failingContract), failSelector); + + address[] memory targetsList; + bytes4[] memory selectorsList; + (targetsList, selectorsList) = blockMeta.getExecutions(); + assertEq(targetsList.length, 2); + assertEq(selectorsList.length, 2); + assertEq(targetsList[0], counterAddress); + assertEq(targetsList[1], address(failingContract)); + assertEq(selectorsList[0], selector); + assertEq(selectorsList[1], failSelector); + + uint256[] memory executionOrder = new uint256[](1); + executionOrder[0] = packExecution(address(failingContract), failSelector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + + (targetsList, selectorsList) = blockMeta.getExecutions(); + assertEq(targetsList.length, 1); + assertEq(selectorsList.length, 1); + assertEq(targetsList[0], address(failingContract)); + assertEq(selectorsList[0], failSelector); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if total gas exceeds the block prologue gas cap. + function testUpdateExecutionOrderRevertsIfGasCapExceeded() public { + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(counterAddress, selector, 600_000); + executionOrder[1] = packExecution(counterAddress, bytes4(keccak256("foo()")), 600_000); + + vm.prank(admin); + vm.expectRevert(IBlockMeta.GasCapExceeded.selector); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' accepts a total exactly at the 63/64 + /// forwarding-rule bound of 'blockPrologueGasCap' (1_000_000 set in setUp -> bound is + /// exactly 984_375). + function testUpdateExecutionOrderAtExact63_64BoundarySucceeds() public { + uint256[] memory executionOrder = new uint256[](1); + executionOrder[0] = packExecution(counterAddress, selector, 984_375); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + + assertEq(blockMeta.totalGasAllocated(), 984_375); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts one gas unit above the 63/64 + /// forwarding-rule bound, even though it is still within the raw 'blockPrologueGasCap'. + function testUpdateExecutionOrderOneGasAbove63_64BoundaryReverts() public { + uint256[] memory executionOrder = new uint256[](1); + executionOrder[0] = packExecution(counterAddress, selector, 984_376); + + vm.prank(admin); + vm.expectRevert(IBlockMeta.GasCapExceeded.selector); + blockMeta.updateExecutionOrder(executionOrder); + } + + /// @dev Test to ensure 'updateExecutionOrder' replaces (rather than accumulates on top of) + /// the previously tracked 'totalGasAllocated', even when entries already existed. + function testUpdateExecutionOrderReplacesTotalGasAllocated() public { + register(counterAddress, selector, DEFAULT_GAS); + register(counterAddress, bytes4(keccak256("foo()")), DEFAULT_GAS); + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS * 2); + + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + uint256[] memory executionOrder = new uint256[](1); + executionOrder[0] = packExecution(address(failingContract), failSelector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.updateExecutionOrder(executionOrder); + + // If the running total carried over the pre-existing allocation instead of being + // reset, this would read DEFAULT_GAS * 3 instead of DEFAULT_GAS. + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS); + } + + /// @dev Test to ensure 'updateExecutionOrder' reverts if passed an empty array. + function testUpdateExecutionOrderRevertsIfEmptyArray() public { + vm.prank(admin); + vm.expectRevert(IBlockMeta.InvalidExecutionsLength.selector); + blockMeta.updateExecutionOrder(new uint256[](0)); + } + + /// @dev Test to ensure 'blockPrologue' executes. + function testBlockPrologue() public { + assertEq(counter.counter(), 0); + testRegister(); + + vm.prank(LibUtils.VM_SIGNER); + blockMeta.blockPrologue(); + assertEq(counter.counter(), 1); + } + + /// @dev Test to ensure 'blockPrologue' reverts if caller is not VM Signer. + function testBlockPrologueRevertsIfNotVmSigner() public { + vm.expectRevert(IBlockMeta.CallerNotVmSigner.selector); + + vm.prank(alice); + blockMeta.blockPrologue(); + } + + /// @dev Test to ensure 'blockPrologue' emits 'CallFailed' when a registered function reverts. + function testBlockPrologueEmitsCallFailed() public { + // Deploy a contract with a failing function + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(address(failingContract), failSelector); + + vm.expectEmit(true, true, false, true); + emit IBlockMeta.CallFailed(address(failingContract), failSelector, abi.encodeWithSignature("Fail()")); + + vm.prank(LibUtils.VM_SIGNER); + blockMeta.blockPrologue(); + } + + /// @dev Test to ensure 'blockPrologue' emits 'CallSucceeded' for a successful call. + function testBlockPrologueEmitsCallSucceeded() public { + register(counterAddress, selector); + + vm.expectEmit(true, true, false, false); + emit IBlockMeta.CallSucceeded(counterAddress, selector); + + vm.prank(LibUtils.VM_SIGNER); + blockMeta.blockPrologue(); + } + + /// @dev Test to ensure 'blockPrologue' continues execution even if a call fails. + function testBlockPrologueContinuesAfterACallFails() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(address(failingContract), failSelector); + register(counterAddress, selector); + + assertEq(counter.counter(), 0); + + // Expect the failing call event + vm.expectEmit(true, true, false, true); + emit IBlockMeta.CallFailed(address(failingContract), failSelector, abi.encodeWithSignature("Fail()")); + + // Expect the successful call event + vm.expectEmit(true, true, false, false); + emit IBlockMeta.CallSucceeded(counterAddress, selector); + + vm.prank(LibUtils.VM_SIGNER); + blockMeta.blockPrologue(); + + // Counter must still be incremented even though the first call failed + assertEq(counter.counter(), 1); + } + + /// @dev Verifies the per-call gas cap is enforced in blockPrologue. + /// Registers three entries: one with sufficient gas (succeeds), + /// one with starved gas (fails, OOG), and another with sufficient gas (succeeds). + /// Asserts that a gas-starved call does not halt execution of subsequent entries. + function testBlockPrologueEnforcesGasLimit() public { + bytes4 updateSelector = Counter.update.selector; + bytes4 viewSelector = Counter.isNotDivisibleBy3.selector; + + // 1. Sufficient gas + register(counterAddress, selector, 100_000); + // 2. Starved gas — call fails + register(counterAddress, updateSelector, 200); + // 3. Sufficient gas — proves loop continues + register(counterAddress, viewSelector, 100_000); + + vm.expectEmit(true, true, false, false); + emit IBlockMeta.CallSucceeded(counterAddress, selector); + + vm.expectEmit(true, true, false, true); + emit IBlockMeta.CallFailed(counterAddress, updateSelector, ""); + + vm.expectEmit(true, true, false, false); + emit IBlockMeta.CallSucceeded(counterAddress, viewSelector); + + vm.prank(LibUtils.VM_SIGNER); + blockMeta.blockPrologue(); + + assertEq(counter.counter(), 1); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' reverts if caller is not the owner. + function testSetBlockPrologueGasCapRevertsIfNotOwner() public { + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + blockMeta.setBlockPrologueGasCap(500_000); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' reverts if cap is zero. + function testSetBlockPrologueGasCapRevertsIfZero() public { + vm.prank(admin); + vm.expectRevert(IBlockMeta.InvalidGasCap.selector); + blockMeta.setBlockPrologueGasCap(0); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' reverts if new cap is below current total allocated gas. + function testSetBlockPrologueGasCapRevertsIfBelowAllocated() public { + register(counterAddress, selector, 50_000); + + vm.prank(admin); + vm.expectRevert(IBlockMeta.InvalidGasCap.selector); + blockMeta.setBlockPrologueGasCap(40_000); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' accepts a cap whose 63/64 forwarding-rule + /// bound exactly covers the already-allocated budget (50_794 -> floor(50_794*63/64) == + /// 50_000 == DEFAULT_GAS). + function testSetBlockPrologueGasCapAtExact63_64BoundarySucceeds() public { + register(counterAddress, selector, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.setBlockPrologueGasCap(50_794); + + assertEq(blockMeta.blockPrologueGasCap(), 50_794); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' reverts one gas unit below the 63/64 + /// boundary (50_793 -> floor(50_793*63/64) == 49_999 < DEFAULT_GAS == 50_000). + function testSetBlockPrologueGasCapJustBelow63_64BoundaryReverts() public { + register(counterAddress, selector, DEFAULT_GAS); + + vm.prank(admin); + vm.expectRevert(IBlockMeta.InvalidGasCap.selector); + blockMeta.setBlockPrologueGasCap(50_793); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' updates the cap. + function testSetBlockPrologueGasCap() public { + vm.prank(admin); + blockMeta.setBlockPrologueGasCap(500_000); + assertEq(blockMeta.blockPrologueGasCap(), 500_000); + } + + /// @dev Test to ensure 'setBlockPrologueGasCap' emits 'BlockPrologueGasCapUpdated'. + function testSetBlockPrologueGasCapEmitsEvent() public { + uint64 newCap = 500_000; + + vm.expectEmit(true, false, false, false); + emit IBlockMeta.BlockPrologueGasCapUpdated(newCap); + + vm.prank(admin); + blockMeta.setBlockPrologueGasCap(newCap); + } + + /// @dev Test to ensure 'blockPrologueGasCap' returns the gas cap set. + function testBlockPrologueGasCap() public { + assertEq(blockMeta.blockPrologueGasCap(), 1_000_000); + } + + /// @dev Test to ensure 'totalGasAllocated' returns the sum of all registered gas limits. + function testTotalGasAllocated() public { + assertEq(blockMeta.totalGasAllocated(), 0); + + register(counterAddress, selector, 50_000); + assertEq(blockMeta.totalGasAllocated(), 50_000); + + vm.prank(admin); + blockMeta.deregister(counterAddress, selector); + assertEq(blockMeta.totalGasAllocated(), 0); + } + + /// @dev Test to ensure 'register' succeeds again once 'deregister' frees enough budget + /// under a gas cap that was fully consumed. + function testRegisterSucceedsAfterDeregisterFreesBudget() public { + // 50_794 is the smallest cap whose 63/64-forwarding-rule bound + // (floor(50_794 * 63 / 64) == 50_000) exactly covers a single DEFAULT_GAS entry, + // so it fits one but not two. + uint64 cap = 50_794; + vm.prank(admin); + blockMeta.setBlockPrologueGasCap(cap); + + register(counterAddress, selector, DEFAULT_GAS); + + bytes4 foo = bytes4(keccak256("foo()")); + vm.expectRevert(IBlockMeta.GasCapExceeded.selector); + register(counterAddress, foo, DEFAULT_GAS); + + vm.prank(admin); + blockMeta.deregister(counterAddress, selector); + + register(counterAddress, foo, DEFAULT_GAS); + + assertEq(blockMeta.totalGasAllocated(), DEFAULT_GAS); + (address[] memory targets, bytes4[] memory selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 1); + assertEq(targets[0], counterAddress); + assertEq(selectors[0], foo); + } + + /// @dev Test to ensure 'getExecutions' returns the execution order. + function testGetExecutions() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + bytes4 foo = bytes4(keccak256("foo()")); + + register(counterAddress, selector); + register(address(failingContract), failSelector); + register(counterAddress, foo); + + (address[] memory targets, bytes4[] memory selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 3); + assertEq(targets[0], counterAddress); + assertEq(targets[1], address(failingContract)); + assertEq(targets[2], counterAddress); + + assertEq(selectors.length, 3); + assertEq(selectors[0], selector); + assertEq(selectors[1], failSelector); + assertEq(selectors[2], foo); + } + + /// @dev Test to ensure 'getTargetContracts' works correctly. + function testGetTargetContracts() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(counterAddress, selector); + register(address(failingContract), failSelector); + register(counterAddress, bytes4(keccak256("foo()"))); + + address[] memory targets = blockMeta.getTargetContracts(); + assertEq(targets.length, 2); + assertEq(targets[0], counterAddress); + assertEq(targets[1], address(failingContract)); + } + + /// @dev Test to ensure 'getSelectors' works correctly. + function testGetSelectors() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + bytes4 foo = bytes4(keccak256("foo()")); + + register(counterAddress, selector); + register(address(failingContract), failSelector); + register(counterAddress, foo); + + bytes4[] memory selectors = blockMeta.getSelectors(counterAddress); + assertEq(selectors.length, 2); + assertEq(selectors[0], selector); + assertEq(selectors[1], foo); + } + + /// @dev Test to ensure 'getExecutionAt' returns an execution entry. + function testGetExecutionAt() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(counterAddress, selector); + register(address(failingContract), failSelector); + + (address target, bytes4 sel) = blockMeta.getExecutionAt(1); + assertEq(target, address(failingContract)); + assertEq(sel, failSelector); + } + + /// @dev Test to ensure 'getExecutionAt' reverts if invalid index is passed. + function testGetExecutionAtRevertsIfInvalidIndex() public { + testRegister(); + + vm.expectRevert(IBlockMeta.InvalidIndex.selector); + blockMeta.getExecutionAt(1); + } + + /// @dev Test to ensure 'getExecutionIndex' returns the index for a target address and selector. + function testGetExecutionIndex() public { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + register(counterAddress, selector); + register(address(failingContract), failSelector); + + assertEq(blockMeta.getExecutionIndex(address(failingContract), failSelector), 1); + } + + /// @dev Test to ensure 'getExecutionIndex' reverts if selector does not exist. + function testGetExecutionIndexRevertsIfSelectorDoesNotExist() public { + vm.expectRevert(IBlockMeta.SelectorNotRegistered.selector); + + blockMeta.getExecutionIndex(counterAddress, selector); + } + + /// @dev Test to ensure 'getExecutionGasLimit' returns the correct gas limit. + function testGetExecutionGasLimit() public { + register(counterAddress, selector, 80_000); + assertEq(blockMeta.getExecutionGasLimit(counterAddress, selector), 80_000); + } + + /// @dev Test to ensure 'getExecutionGasLimit' reverts if the pair is not registered. + function testGetExecutionGasLimitRevertsIfNotRegistered() public { + vm.expectRevert(IBlockMeta.SelectorNotRegistered.selector); + blockMeta.getExecutionGasLimit(counterAddress, selector); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'upgradeToAndCall' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'upgradeToAndCall' upgrades the proxy to a new implementation. + function testUpgradeToAndCall() public { + register(counterAddress, selector); + + vm.startPrank(admin); + BlockMeta newImpl = new BlockMeta(); + blockMeta.upgradeToAndCall(address(newImpl), ""); + vm.stopPrank(); + + assertEq(address(uint160(uint256(vm.load(address(blockMeta), ERC1967Utils.IMPLEMENTATION_SLOT)))), address(newImpl)); + + register(counterAddress, bytes4(keccak256("foo()"))); + (address[] memory targets, bytes4[] memory selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 2); + assertEq(selectors.length, 2); + } + + /// @dev Test to ensure 'upgradeToAndCall' reverts if caller is not the owner. + function testUpgradeToAndCallRevertsIfNotOwner() public { + vm.prank(admin); + BlockMeta newImpl = new BlockMeta(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + vm.prank(alice); + blockMeta.upgradeToAndCall(address(newImpl), ""); + } + + /// @dev Helper function to pack a target contract address, function selector, and gas limit into a single uint256 execution entry. + function packExecution(address _targetContract, bytes4 _selector, uint64 _gasLimit) private pure returns (uint256) { + // Layout: [target[160] | selector[32] | gasLimit[64]] + return (uint256(uint160(_targetContract)) << 96) | (uint256(uint32(_selector)) << 64) | _gasLimit; + } + + /// @dev Helper function to return an execution order. + function createExecutionOrder() private returns (uint256[] memory) { + FailingContract failingContract = new FailingContract(); + bytes4 failSelector = FailingContract.fail.selector; + + uint256[] memory executionOrder = new uint256[](2); + executionOrder[0] = packExecution(address(failingContract), failSelector, DEFAULT_GAS); + executionOrder[1] = packExecution(counterAddress, selector, DEFAULT_GAS); + + return executionOrder; + } +} + +contract FailingContract { + error Fail(); + function fail() external pure { + revert Fail(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/ConfigFacet.t.sol b/solidity/supra_contracts/test/ConfigFacet.t.sol new file mode 100644 index 0000000000..2d946c610d --- /dev/null +++ b/solidity/supra_contracts/test/ConfigFacet.t.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {BaseDiamondTest, FailingERC20} from "./BaseDiamondTest.t.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {LibDiamond} from "../src/libraries/LibDiamond.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {Config} from "../src/libraries/LibAppStorage.sol"; + +contract ConfigFacetTest is BaseDiamondTest { + + // :::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'grantAuthorization' :::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'grantAuthorization' grants authorization to an address. + function testGrantAuthorization() public { + vm.prank(admin); + IConfigFacet(diamondAddr).grantAuthorization(alice); + + assertTrue(IRegistryFacet(diamondAddr).isAuthorizedSubmitter(alice)); + } + + /// @dev Test to ensure 'grantAuthorization' emits event 'AuthorizationGranted'. + function testGrantAuthorizationEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IConfigFacet.AuthorizationGranted(alice, block.timestamp); + + vm.prank(admin); + IConfigFacet(diamondAddr).grantAuthorization(alice); + } + + /// @dev Test to ensure 'grantAuthorization' reverts if address is already authorized. + function testGrantAuthorizationRevertsIfAlreadyAuthorised() public { + // Grant authorization to alice + testGrantAuthorization(); + + vm.expectRevert(IConfigFacet.AddressAlreadyExists.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).grantAuthorization(alice); + } + + /// @dev Test to ensure 'grantAuthorization' reverts if caller is not owner. + function testGrantAuthorizationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).grantAuthorization(alice); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'revokeAuthorization' ::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'revokeAuthorization' revokes authorization from an address. + function testRevokeAuthorization() public { + // Grant authorization to alice + testGrantAuthorization(); + + // Revoke authorization + vm.prank(admin); + IConfigFacet(diamondAddr).revokeAuthorization(alice); + + assertFalse(IRegistryFacet(diamondAddr).isAuthorizedSubmitter(alice)); + } + + /// @dev Test to ensure 'revokeAuthorization' emits event 'AuthorizationRevoked'. + function testRevokeAuthorizationEmitsEvent() public { + // Grant authorization to alice + testGrantAuthorization(); + + vm.expectEmit(true, true, false, false); + emit IConfigFacet.AuthorizationRevoked(alice, block.timestamp); + + vm.prank(admin); + IConfigFacet(diamondAddr).revokeAuthorization(alice); + } + + /// @dev Test to ensure 'revokeAuthorization' reverts if address is not authorised. + function testRevokeAuthorizationRevertsIfNotAuthorised() public { + vm.expectRevert(IConfigFacet.AddressDoesNotExist.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).revokeAuthorization(alice); + } + + /// @dev Test to ensure 'revokeAuthorization' reverts if caller is not owner. + function testRevokeAuthorizationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).revokeAuthorization(alice); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'disableRegistration' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'disableRegistration' disables the registration. + function testDisableRegistration() public { + vm.prank(admin); + IConfigFacet(diamondAddr).disableRegistration(); + + assertFalse(IConfigFacet(diamondAddr).isRegistrationEnabled()); + } + + /// @dev Test to ensure 'disableRegistration' emits event 'TaskRegistrationDisabled'. + function testDisableRegistrationEmitsEvent() public { + vm.expectEmit(true, false, false, false); + emit IConfigFacet.TaskRegistrationDisabled(false); + + testDisableRegistration(); + } + + /// @dev Test to ensure 'disableRegistration' reverts if registration is already disabled. + function testDisableRegistrationRevertsIfAlreadyDisabled() public { + // Disable registration + testDisableRegistration(); + + // Disable again → revert + vm.expectRevert(IConfigFacet.AlreadyDisabled.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).disableRegistration(); + } + + /// @dev Test to ensure 'disableRegistration' reverts if caller is not owner. + function testDisableRegistrationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).disableRegistration(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'enableRegistration' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'enableRegistration' enables the registration. + function testEnableRegistration() public { + // Disable registration + testDisableRegistration(); + + // Enable registration + vm.prank(admin); + IConfigFacet(diamondAddr).enableRegistration(); + + assertTrue(IConfigFacet(diamondAddr).isRegistrationEnabled()); + } + + /// @dev Test to ensure 'enableRegistration' emits event 'TaskRegistrationEnabled'. + function testEnableRegistrationEmitsEvent() public { + // Disable registration + testDisableRegistration(); + + vm.expectEmit(true, false, false, false); + emit IConfigFacet.TaskRegistrationEnabled(true); + + // Enable registration + vm.prank(admin); + IConfigFacet(diamondAddr).enableRegistration(); + } + + /// @dev Test to ensure 'enableRegistration' reverts if registration is already enabled. + function testEnableRegistrationRevertsIfAlreadyEnabled() public { + vm.expectRevert(IConfigFacet.AlreadyEnabled.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).enableRegistration(); + } + + /// @dev Test to ensure 'enableRegistration' reverts if caller is not owner. + function testEnableRegistrationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).enableRegistration(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'withdrawFees' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'withdrawFees' reverts if amount is zero. + function testWithdrawFeesRevertsIfAmountZero() public { + vm.prank(admin); + + vm.expectRevert(IConfigFacet.InvalidAmount.selector); + IConfigFacet(diamondAddr).withdrawFees(0, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if recipient address is zero. + function testWithdrawFeesRevertsIfRecipientAddressZero() public { + vm.prank(admin); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + IConfigFacet(diamondAddr).withdrawFees(1 ether, address(0)); + } + + /// @dev Test to ensure 'withdrawFees' reverts if contract has insufficient balance. + function testWithdrawFeesRevertsIfInsufficientBalance() public { + vm.expectRevert(IConfigFacet.InsufficientBalance.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(1 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if request amount exceeds the locked balance. + function testWithdrawFeesRevertsIfRequestExceedsLockedBalance() public { + registerUst(diamondAddr, 2450); + + vm.expectRevert(IConfigFacet.RequestExceedsLockedBalance.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(2 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if caller is not owner. + function testWithdrawFeesRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).withdrawFees(1 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' withdraws the requested amount and updates the balance. + function testWithdrawFees() public { + registerUst(diamondAddr, 2450); + + assertEq(erc20Supra.balanceOf(admin), 0); + assertEq(erc20Supra.balanceOf(diamondAddr), 61.1 ether); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(1 ether, admin); + + assertEq(erc20Supra.balanceOf(admin), 1 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 60.1 ether); + } + + /// @dev Test to ensure 'withdrawFees' emits event 'RegistryFeeWithdrawn'. + function testWithdrawFeesEmitsEvent() public { + registerUst(diamondAddr, 2450); + + vm.expectEmit(true, true, false, false); + emit IConfigFacet.RegistryFeeWithdrawn(admin, 0.002 ether); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(0.002 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if ERC20 transfer fails. + function testWithdrawFeesRevertsIfTransferFails() public { + FailingERC20 failingToken = new FailingERC20(); + vm.etch(address(erc20Supra), address(failingToken).code); + + vm.expectRevert(IConfigFacet.TransferFailed.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(1 ether, admin); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'updateConfigBuffer' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function that returns a valid config. + function validConfig() private pure returns (Config memory cfg) { + cfg = Config({ + registryMaxGasCap: 10_000_000, + sysRegistryMaxGasCap: 5_000_000, + automationBaseFeeWeiPerSec: 0.001 ether, + flatRegistrationFeeWei: 0.002 ether, + congestionBaseFeeWeiPerSec: 0.002 ether, + taskDurationCapSecs: 3600, + sysTaskDurationCapSecs: 3600, + cycleDurationSecs: 2000, + taskCapacity: 500, + sysTaskCapacity: 500, + congestionThresholdPercentage: 55, + congestionExponent: 3, + maxCongestionExponent: 6 + }); + } + + /// @dev Test to ensure 'updateConfigBuffer' updates the config buffer. + function testUpdateConfigBuffer() public { + Config memory cfg = validConfig(); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + + // Pending config should be updated + Config memory configBuffer = IConfigFacet(diamondAddr).getConfigBuffer(); + assertEq(configBuffer.taskDurationCapSecs, cfg.taskDurationCapSecs); + assertEq(configBuffer.registryMaxGasCap, cfg.registryMaxGasCap); + assertEq(configBuffer.automationBaseFeeWeiPerSec, cfg.automationBaseFeeWeiPerSec); + assertEq(configBuffer.flatRegistrationFeeWei, cfg.flatRegistrationFeeWei); + assertEq(configBuffer.congestionThresholdPercentage, cfg.congestionThresholdPercentage); + assertEq(configBuffer.congestionBaseFeeWeiPerSec, cfg.congestionBaseFeeWeiPerSec); + assertEq(configBuffer.congestionExponent, cfg.congestionExponent); + assertEq(configBuffer.maxCongestionExponent, cfg.maxCongestionExponent); + assertEq(configBuffer.taskCapacity, cfg.taskCapacity); + assertEq(configBuffer.cycleDurationSecs, cfg.cycleDurationSecs); + assertEq(configBuffer.sysTaskDurationCapSecs, cfg.sysTaskDurationCapSecs); + assertEq(configBuffer.sysRegistryMaxGasCap, cfg.sysRegistryMaxGasCap); + assertEq(configBuffer.sysTaskCapacity, cfg.sysTaskCapacity); + } + + /// @dev Test to ensure 'updateConfigBuffer' emits event 'ConfigBufferUpdated'. + function testUpdateConfigBufferEmitsEvent() public { + Config memory cfg = validConfig(); + + vm.expectEmit(true, false, false, false); + emit IConfigFacet.ConfigBufferUpdated(cfg); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts if caller is not owner. + function testUpdateConfigBufferRevertsIfNotOwner() public { + Config memory cfg = validConfig(); + + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts when registryMaxGasCap is less than gas committed for next cycle. + function testUpdateConfigBufferRevertsWhenRegistryMaxGasCapIsLessThanGasCommittedForNextCycle() public { + registerUst(diamondAddr, 2450); + Config memory cfg = validConfig(); + + vm.expectRevert(IConfigFacet.UnacceptableRegistryMaxGasCap.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + 99999, // registryMaxGasCap less than gas committed for next cycle + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts when sysRegistryMaxGasCap is less than system gas committed for next cycle. + function testUpdateConfigBufferRevertsWhenSysRegistryMaxGasCapIsLessThanSysGasCommittedForNextCycle() public { + registerGst(diamondAddr, 2450); + Config memory cfg = validConfig(); + + vm.expectRevert(IConfigFacet.UnacceptableSysRegistryMaxGasCap.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + 99999, // sysRegistryMaxGasCap less than system gas committed for next cycle + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts when congestionExponent exceeds maxCongestionExponent. + function testUpdateConfigBufferRevertsWhenCongestionExponentExceedsMax() public { + Config memory cfg = validConfig(); + + vm.expectRevert(LibCommon.CongestionExponentExceedsMax.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.maxCongestionExponent + 1, // congestionExponent above the cap + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts when maxCongestionExponent is zero. + function testUpdateConfigBufferRevertsWhenMaxCongestionExponentZero() public { + Config memory cfg = validConfig(); + + vm.expectRevert(LibCommon.InvalidMaxCongestionExponent.selector); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + 0, // maxCongestionExponent + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' succeeds when congestionExponent equals maxCongestionExponent exactly. + function testUpdateConfigBufferSucceedsAtCongestionExponentBoundary() public { + Config memory cfg = validConfig(); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.maxCongestionExponent, // congestionExponent == maxCongestionExponent + cfg.maxCongestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + + assertEq(IConfigFacet(diamondAddr).getConfigBuffer().congestionExponent, cfg.maxCongestionExponent); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/CoreFacet.t.sol b/solidity/supra_contracts/test/CoreFacet.t.sol new file mode 100644 index 0000000000..ed5a279ab5 --- /dev/null +++ b/solidity/supra_contracts/test/CoreFacet.t.sol @@ -0,0 +1,1766 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Vm} from "forge-std/Vm.sol"; +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {IDiamondLoupe} from "../src/interfaces/IDiamondLoupe.sol"; +import {IRegistryStatus} from "../src/interfaces/IRegistryStatus.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {LibDiamond} from "../src/libraries/LibDiamond.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; + +contract CoreFacetTest is BaseDiamondTest { + + /// @dev Test to ensure 'monitorCycleEnd' reverts if tx.origin is not VM Signer. + function testMonitorCycleEndRevertsIfTxOriginNotVm() public { + vm.expectRevert(LibUtils.CallerNotVmSigner.selector); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + } + + /// @dev Test to ensure 'monitorCycleEnd' does nothing before cycle expiry. + function testMonitorCycleEndDoesNothingBeforeCycleExpiry() public { + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(stateBefore)); + } + + /// @dev Test to ensure 'monitorCycleEnd' does nothing if state is not STARTED. + function testMonitorCycleEndDoesNothingIfNotStarted() public { + vm.startPrank(admin); + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600, + registryMaxGasCap: 10_000_000, + automationBaseFeeWeiPerSec: 0.001 ether, + flatRegistrationFeeWei: 0.002 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.002 ether, + congestionExponent: 2, + maxCongestionExponent: 6, + taskCapacity: 500, + cycleDurationSecs: 2000, + sysTaskDurationCapSecs: 3600, + sysRegistryMaxGasCap: 5_000_000, + sysTaskCapacity: 500, + registrationEnabled: false, + automationEnabled: false + }); + + Deployment memory deployment = LibDiamondUtils.deploy(admin, address(erc20Supra), initParams); + + address diamondAddr = deployment.diamond; + vm.stopPrank(); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.READY)); + + vm.warp(startBefore + durationBefore); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(stateBefore)); + } + + /// @dev Test to ensure 'monitorCycleEnd' moves cycle state to STARTED if automation is enabled and no tasks exist. + function testMonitorCycleEndWhenAutomationEnabledNoTasks() public { + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore + 1, + LibCommon.CycleState.STARTED, + uint64(block.timestamp), + durationBefore, + stateBefore + ); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + + assertEq(indexAfter, indexBefore + 1); + assertEq(startAfter, block.timestamp); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED)); + } + + /// @dev Test to ensure 'monitorCycleEnd' moves cycle state to FINISHED if automation is enabled and tasks exist. + function testMonitorCycleEndWhenAutomationEnabledAndTasksExist() public { + registerUst(diamondAddr, 2450); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore, + LibCommon.CycleState.FINISHED, + startBefore, + durationBefore, + stateBefore + ); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.FINISHED)); + + (uint64 refundDuration, uint128 automationFeePerSec) = ICoreFacet(diamondAddr).getTransitionInfo(); + assertEq(refundDuration, 0); + assertEq(automationFeePerSec, 0.5 ether); + } + + /// @dev Test to ensure 'processTasks' reverts if caller is not VM Signer. + function testProcessTasksRevertsIfNotVm() public { + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectRevert(LibUtils.CallerNotVmSigner.selector); + + vm.prank(admin); + ICoreFacet(diamondAddr).processTasks(1, tasks); + } + + /// @dev Test to ensure 'processTasks' reverts if state is not FINISHED or SUSPENDED. + function testProcessTasksRevertsIfInvalidState() public { + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectRevert(ICoreFacet.InvalidRegistryState.selector); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(1, tasks); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is FINISHED. + function testProcessTasksWhenCycleStateFinished() public { + registerUst(diamondAddr, 2450); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + processCycleTransition(diamondAddr, tasks); + + (uint64 newIndex, uint64 newStart, uint64 newDuration, LibCommon.CycleState newState) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(newIndex, 2); + assertEq(newStart, uint64(block.timestamp)); + assertEq(newDuration, 1200); + assertEq(uint8(newState), uint8(LibCommon.CycleState.STARTED)); + + assertEq(IRegistryFacet(diamondAddr).getActiveTaskIds(), tasks); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 100000); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForCurrentCycle(), 100000); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + } + + /// @dev Test to ensure 'processTasks' reverts if invalid cycle index is passed when cycle state is FINISHED. + function testProcessTasksRevertsIfInvalidCycleIndexWhenCycleStateFinished() public { + registerUst(diamondAddr, 2450); + + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectRevert(ICoreFacet.InvalidInputCycleIndex.selector); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, tasks); + } + + /// @notice Test to ensure 'processTasks' reverts if tasks are processed out of order. + function testProcessTasksRevertsIfTasksOutOfOrder() public { + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 1; + + vm.expectRevert(ICoreFacet.OutOfOrderTaskProcessingRequest.selector); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + } + + /// @dev Sorting a caller-submitted batch is the downstream (VM_SIGNER) submitter's + /// responsibility, not this contract's — see LibCore.requireSortedAscending's NatSpec. + /// A batch submitted out of order (here, strictly descending) must revert immediately, + /// before any task in it is processed, rather than being silently sorted. + function testProcessTasksRevertsIfBatchNotPreSorted() public { + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , ) = ICoreFacet(diamondAddr).getCycleInfo(); + + uint256[] memory tasks = new uint256[](2); + tasks[0] = 1; + tasks[1] = 0; + + vm.expectRevert(ICoreFacet.OutOfOrderTaskProcessingRequest.selector); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + } + + /// @dev Test to ensure 'processTasks' (SUSPENDED branch, onCycleSuspend) emits RemovedTasks + /// containing only the indexes actually removed, even when the input batch also contains + /// non-existent indexes. + function testOnCycleSuspendEmitsOnlyRemovedTasks() public { + registerUst(diamondAddr, 2450); // task 0 + registerUst(diamondAddr, 2450); // task 1 + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , ) = ICoreFacet(diamondAddr).getCycleInfo(); + + // Process task 0 on its own first, advancing the expected-order position past it, so + // the second batch below cannot be confused with a genuine removal of task 0. + uint256[] memory firstBatch = new uint256[](1); + firstBatch[0] = 0; + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, firstBatch); + + uint256[] memory secondBatch = new uint256[](1); + secondBatch[0] = 1; + + uint64[] memory expectedRemoved = new uint64[](1); + expectedRemoved[0] = 1; + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.RemovedTasks(expectedRemoved); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, secondBatch); + } + + + /// @dev A batch containing a task index outside the expected set (never registered) must + /// revert immediately rather than being treated as a no-op — see dropOrChargeTask's + /// NatSpec. + function testProcessTasksRevertsIfTaskDoesNotExist() public { + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 99; // never registered + + vm.expectRevert(abi.encodeWithSelector(ICoreFacet.UnknownTaskToProcess.selector, uint64(99))); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is disabled. + function testProcessTasksWhenCycleStateSuspendedAutomationDisabled() public { + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.RemovedTasks(tasksUint64); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, tasks); + + ( , , , LibCommon.CycleState newState) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(newState), uint8(LibCommon.CycleState.READY)); + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(tasksUint64[0])); + } + + /// @dev 'processTasks' (SUSPENDED branch, onCycleSuspend) must revert, without applying + /// any partial effect, when the batch mixes a real task with a non-existent one. + function testOnCycleSuspendRevertsIfBatchContainsUnknownTaskAlongsideReal() public { + registerUst(diamondAddr, 2450); // task 0 + registerUst(diamondAddr, 2450); // task 1 + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , ) = ICoreFacet(diamondAddr).getCycleInfo(); + + // Process task 0 on its own first, advancing the expected-order position past it, so + // the second batch below cannot be confused with a genuine removal of task 0. + uint256[] memory firstBatch = new uint256[](1); + firstBatch[0] = 0; + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, firstBatch); + + uint256[] memory secondBatch = new uint256[](2); + secondBatch[0] = 1; + secondBatch[1] = 999; // does not exist + + vm.expectRevert(abi.encodeWithSelector(ICoreFacet.UnknownTaskToProcess.selector, uint64(999))); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, secondBatch); + + // The revert must undo task 1's removal too - no partial effect from the batch. + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(1)); + } + + /// @dev 'processTasks' (SUSPENDED branch, onCycleSuspend) must revert, and leave registry + /// state untouched, when the batch contains only a non-existent task index. + function testOnCycleSuspendRevertsIfBatchContainsOnlyUnknownTask() public { + registerUst(diamondAddr, 2450); // task 0 + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , ) = ICoreFacet(diamondAddr).getCycleInfo(); + + uint256[] memory batch = new uint256[](1); + batch[0] = 999; // does not exist + + vm.expectRevert(abi.encodeWithSelector(ICoreFacet.UnknownTaskToProcess.selector, uint64(999))); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, batch); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is enabled. + function testProcessTasksWhenCycleStateSuspendedAutomationEnabled() public { + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + + // Enable automation + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + uint64[] memory tasksUint64 = new uint64[](1); + tasks[0] = 0; + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.RemovedTasks(tasksUint64); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter, tasks); + + (uint64 newIndex, uint64 newStart, uint64 newDuration, LibCommon.CycleState newState) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(newIndex, indexAfter + 1); + assertEq(newStart, uint64(block.timestamp)); + assertEq(newDuration, 1200); + assertEq(uint8(newState), uint8(LibCommon.CycleState.STARTED)); + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(tasksUint64[0])); + } + + /// @dev Test to ensure 'processTasks' reverts if invalid cycle index is passed when cycle state is SUSPENDED. + function testProcessTasksRevertsIfInvalidCycleIndexWhenCycleStateSuspended() public { + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + (uint64 indexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectRevert(ICoreFacet.InvalidInputCycleIndex.selector); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexAfter + 1, tasks); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'disableAutomation' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'disableAutomation' disables the automation. + function testDisableAutomation() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + } + + /// @dev Test to ensure 'isAutomationReadyEnabled' reflects both isInitialized() (via the + /// diamond) and isAutomationEnabled() -- true only while both hold. + function testIsAutomationReadyEnabledTracksBothInitializedAndEnabled() public { + assertTrue(IRegistryStatus(diamondAddr).isInitialized()); + assertTrue(ICoreFacet(diamondAddr).isAutomationEnabled()); + assertTrue(ICoreFacet(diamondAddr).isAutomationReadyEnabled()); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + // Still initialized, but no longer "ready" since automation is disabled. + assertTrue(IRegistryStatus(diamondAddr).isInitialized()); + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + assertFalse(ICoreFacet(diamondAddr).isAutomationReadyEnabled()); + } + + /// @dev Test to ensure 'isAutomationReadyEnabled' is a normally-routed selector. + function testIsAutomationReadyEnabledSelectorRouting() public view { + assertEq( + IDiamondLoupe(diamondAddr).facetAddress(ICoreFacet.isAutomationReadyEnabled.selector), + deployment.facets.coreFacet + ); + } + + /// @dev Test to ensure 'disableAutomation' emits event 'AutomationDisabled'. + function testDisableAutomationEmitsEvent() public { + vm.expectEmit(true, false, false, false); + emit ICoreFacet.AutomationDisabled(false); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + } + + /// @dev Test to ensure 'disableAutomation' reverts if automation is already disabled. + function testDisableAutomationRevertsIfAlreadyDisabled() public { + // Disable automation + testDisableAutomation(); + + // Disable again → revert + vm.expectRevert(ICoreFacet.AlreadyDisabled.selector); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + } + + /// @dev Test to ensure 'disableAutomation' reverts if caller is not owner. + function testDisableAutomationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + ICoreFacet(diamondAddr).disableAutomation(); + } + + /// @dev Test to ensure 'disableAutomation' moves cycle state from STARTED to READY if automation is disabled and no tasks exist. + function testDisableAutomationStartedToReadyWhenNoTasksExist() public { + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.STARTED)); + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore, + LibCommon.CycleState.READY, + startBefore, + durationBefore, + stateBefore + ); + + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexBefore, indexAfter); + assertEq(startBefore, startAfter); + assertEq(durationBefore, durationAfter); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.READY)); + } + + /// @dev Test to ensure 'disableAutomation' moves cycle state from STARTED to SUSPENDED if automation is disabled and tasks exist. + function testDisableAutomationStartedToSuspendedWhenTasksExist() public { + registerUst(diamondAddr, 2450); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.STARTED)); + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore, + LibCommon.CycleState.SUSPENDED, + startBefore, + durationBefore, + stateBefore + ); + + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexBefore, indexAfter); + assertEq(startBefore, startAfter); + assertEq(durationBefore, durationAfter); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + } + + /// @dev Test to ensure 'disableAutomation' moves cycle state from FINISHED to SUSPENDED if automation is disabled and transition is not started. + function testDisableAutomationFinishedToSuspendedWhenTransitionNotStarted() public { + registerUst(diamondAddr, 2450); + + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + // Moves state to FINISHED + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore, + LibCommon.CycleState.SUSPENDED, + startBefore, + durationBefore, + stateBefore + ); + + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexBefore, indexAfter); + assertEq(startBefore, startAfter); + assertEq(durationBefore, durationAfter); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + } + + /// @dev Test to ensure 'disableAutomation' does not change cycle state from FINISHED if automation is disabled and transition is in progress. + function testDisableAutomationRetainsFinishedStateIfTransitionInProgress() public { + // Register 2 USTs so transition requires processing both + registerUst(diamondAddr, 2450); // task index 0 + registerUst(diamondAddr, 2450); // task index 1 + + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + // Move state to FINISHED + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Process only task 0 — transition is now in progress + uint256[] memory partialTasks = new uint256[](1); + partialTasks[0] = 0; + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, partialTasks); + + // Cycle state is FINISHED (transition not yet complete) + ( , , , LibCommon.CycleState stateAfterPartial) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfterPartial), uint8(LibCommon.CycleState.FINISHED)); + + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.FINISHED)); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'enableAutomation' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'enableAutomation' enables the automation. + function testEnableAutomation() public { + // Disable automation + testDisableAutomation(); + + // Enable automation + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + + assertTrue(ICoreFacet(diamondAddr).isAutomationEnabled()); + } + + /// @dev Test to ensure 'enableAutomation' emits event 'AutomationEnabled'. + function testEnableAutomationEmitsEvent() public { + // Disable automation + testDisableAutomation(); + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.AutomationEnabled(true); + + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + } + + /// @dev Test to ensure 'enableAutomation' reverts if automation is already enabled. + function testEnableAutomationRevertsIfAlreadyEnabled() public { + vm.expectRevert(ICoreFacet.AlreadyEnabled.selector); + + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + } + + /// @dev Test to ensure 'enableAutomation' reverts if caller is not owner. + function testEnableAutomationRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + ICoreFacet(diamondAddr).enableAutomation(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to sys gas cycle accounting :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Registers a GST with an explicit maxGasAmount (registerGst hardcodes 100_000). + function registerGstWithGas(address _diamond, uint64 _duration, uint128 _maxGasAmount) internal { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(_diamond); + + vm.prank(bob); + IRegistryFacet(_diamond).registerSystemTask( + payload, + predicate, + uint64(block.timestamp + _duration), + _maxGasAmount, + 2, + auxData + ); + } + + /// @dev Like BaseDiamondTest.processCycleTransition, but without asserting the ActiveTasks + /// event echoes `_taskIndexes` verbatim — that assumption breaks when one of the processed + /// tasks expires and gets removed during this very transition, since the real ActiveTasks + /// list then only contains the tasks that survived. + function processCycleTransitionAllowingRemovals(address _diamond, uint256[] memory _taskIndexes) internal { + (uint64 indexBefore, uint64 startTimeBefore, uint64 durationBefore,) = ICoreFacet(_diamond).getCycleInfo(); + vm.warp(startTimeBefore + durationBefore); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(_diamond).monitorCycleEnd(); + ICoreFacet(_diamond).processTasks(indexBefore + 1, _taskIndexes); + vm.stopPrank(); + } + + /// @dev Proves sysGasCommittedForThisCycle correctly reflects a GST task that expires mid-cycle + /// (rather than reporting the commitment for the cycle after the current one, as an earlier + /// review concern claimed). Task A survives many cycles; task B expires partway through cycle 2. + /// sysGasCommittedForThisCycle must be 150,000 (both active) at cycle 2's start, then settle to + /// 100,000 (only A) once B has actually expired and been removed. + function testSysGasCommittedForThisCycleReflectsMidCycleExpiringTask() public { + registerGstWithGas(diamondAddr, 12_000, 100_000); // task 0: survives many cycles + registerGstWithGas(diamondAddr, 1_800, 50_000); // task 1: expires partway through cycle 2 + + uint256[] memory bothTasks = new uint256[](2); + bothTasks[0] = 0; + bothTasks[1] = 1; + + // Cycle 1 -> 2: both tasks still active and unexpired when this transition runs. + processCycleTransition(diamondAddr, bothTasks); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 150_000); + + // Cycle 2 -> 3: task 1 has now expired and gets removed during this transition. + processCycleTransitionAllowingRemovals(diamondAddr, bothTasks); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 100_000); + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(1)); + + // Cycle 3 -> 4: only task 0 remains. + uint256[] memory onlyTaskA = new uint256[](1); + onlyTaskA[0] = 0; + processCycleTransition(diamondAddr, onlyTaskA); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 100_000); + } + + /// @dev Proves sysGasCommittedForThisCycle is forced to 0 when a suspended cycle finalizes, + /// rather than rotating in the pre-wipe accumulated value. Suspension removes every task, so + /// the cycle about to start genuinely has zero commitment left. + function testSysGasCommittedForThisCycleIsZeroAfterSuspend() public { + registerGstWithGas(diamondAddr, 12_000, 100_000); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(1, taskIndexes); + + (,,, LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.READY)); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 0); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'removeRegisteredTask' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'removeRegisteredTask' removes a UST when predicate validation fails and reduces the gasCommittedForNextCycle. + function testRemoveRegisteredTasksForUST() public { + // Register two USTs + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(1)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 2); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 200_000); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 120.2 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 122.2 ether); + assertEq(erc20Supra.balanceOf(alice), 77.8 ether); + + uint256[] memory taskIndexes = new uint256[](2); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + processCycleTransition(diamondAddr, taskIndexes); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 6 ether); + + // Remove only task 0 due to predicate failure, cycle index is 2 + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + + // Verify only task 0 is removed; task 1 remains with its gas committed + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(1)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 100_000); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 66.6 ether); + assertEq(erc20Supra.balanceOf(alice), 133.4 ether); + } + + /// @dev Test to ensure 'removeRegisteredTask' removes a GST when predicate validation fails and reduces the systemGasCommittedForNextCycle. + function testRemoveRegisteredTasksForGST() public { + // Register two GSTs + registerGst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + assertTrue(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertTrue(IRegistryFacet(diamondAddr).ifSysTaskExists(1)); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 2); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 200_000); + + uint256[] memory taskIndexes = new uint256[](2); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + processCycleTransition(diamondAddr, taskIndexes); + + // Remove only task 0 due to predicate failure + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + + // Verify only task 0 is removed; task 1 remains with its gas committed + assertFalse(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertTrue(IRegistryFacet(diamondAddr).ifSysTaskExists(1)); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 100_000); + } + + /// @dev Test to ensure 'removeRegisteredTask' emits 'TaskRemovedBySystem' event. + function testRemoveRegisteredTasksEmitsEvent() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + processCycleTransition(diamondAddr, taskIndexes); + + LibCommon.RemovedTask memory removedTask = LibCommon.RemovedTask(0, LibCommon.TaskType.UST, alice, keccak256("txHash"), "Predicate failed"); + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.TaskRemovedBySystem(removedTask); + + // Remove task due to predicate failure + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts if caller is not VM Signer. + function testRemoveRegisteredTasksRevertsIfNotVmSigner() public { + registerUst(diamondAddr, 2450); + + vm.expectRevert(LibUtils.CallerNotVmSigner.selector); + + uint64 taskIndex = 0; + string memory reason = "Predicate failed"; + + vm.prank(alice); + ICoreFacet(diamondAddr).removeRegisteredTask(2, taskIndex, reason); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts if cycle index is incorrect. + function testRemoveRegisteredTasksRevertsIfCycleIndexIncorrect() public { + registerUst(diamondAddr, 2450); + + vm.expectRevert(ICoreFacet.InvalidInputCycleIndex.selector); + + uint64 taskIndex = 0; + string memory reason = "Predicate failed"; + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(2, taskIndex, reason); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts if cycle index is incorrect. + function testRemoveRegisteredTasksRevertsIfCycleIndexIncorrect2() public { + registerUst(diamondAddr, 2450); + + vm.expectRevert(ICoreFacet.InvalidInputCycleIndex.selector); + + uint64 taskIndex = 0; + string memory reason = "Predicate failed"; + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(0, taskIndex, reason); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts when automation is disabled (cycle not STARTED). + function testRemoveRegisteredTaskRevertsWhenAutomationDisabled() public { + registerUst(diamondAddr, 2450); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + + vm.expectRevert(ICoreFacet.InvalidOperationForCurrentCycleState.selector); + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(1, 0, "Predicate failed"); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts when the cycle is FINISHED (mid-transition), + /// not just when automation is disabled. Without this guard, a removal landing mid-transition would + /// desync markTaskProcessed's expected-order bookkeeping and permanently wedge the cycle transition + /// (every subsequent processTasks call would revert with OutOfOrderTaskProcessingRequest). + function testRemoveRegisteredTaskRevertsWhenCycleFinished() public { + registerUst(diamondAddr, 2450); + + (uint64 indexBefore, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (,,, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.FINISHED)); + + vm.expectRevert(ICoreFacet.InvalidOperationForCurrentCycleState.selector); + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(indexBefore, 0, "Predicate failed"); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @notice Test to ensure removeRegisteredTask reverts with InsufficientBalanceForRefund if registry has insufficient balance. + function testRemoveRegisteredTaskRevertsIfInsufficientBalance() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + uint256 diamondBalance = erc20Supra.balanceOf(diamondAddr); + vm.prank(diamondAddr); + erc20Supra.transfer(address(0xdead), diamondBalance); + assertEq(erc20Supra.balanceOf(diamondAddr), 0); + + vm.expectRevert(ICoreFacet.InsufficientBalanceForRefund.selector); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(2, 0, "Predicate failed"); + } + + /// @notice Test to ensure that when automation is disabled mid-transition (FINISHED, some tasks + /// remaining), suspension is deferred until the transition completes and the new cycle starts. + function testDisableAutomationDefersSuspensionUntilTransitionEnds() public { + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexBefore, , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Process only task 0 — transition in progress + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, taskIndexes); + + ( , , , stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Disable automation — deferred because transition is in progress + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + // Process task 1 — finalizes transition → new cycle STARTED → then SUSPENDED + taskIndexes[0] = 1; + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, taskIndexes); + + (uint64 indexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexAfter, indexBefore + 1); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + } + + /// @dev Test to ensure 'getCycleStateDetails' returns correct cycle details. + function testGetCycleStateDetails() public { + registerUst(diamondAddr, 2450); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(details.index, indexBefore); + assertEq(details.startTime, startBefore); + assertEq(details.durationSecs, durationBefore); + assertEq(uint8(details.state), uint8(LibCommon.CycleState.FINISHED)); + assertEq(details.nextTaskIndexPosition, 0); + assertEq(details.expectedTasksToBeProcessed.length, 1); + assertEq(details.expectedTasksToBeProcessed[0], 0); + } + + /// @dev Proves transitionState.nextTaskIndexPosition/expectedTasksToBeProcessed don't survive + /// stale into a normal STARTED cycle after a populated FINISHED->STARTED transition. Also + /// proves isTransitionInProgress()-consulting disableAutomation doesn't misbehave right after + /// — a stale ifTransitionStateExists would misroute tryMoveToSuspendedState into asserting + /// cycleState==FINISHED, which would revert since the cycle is genuinely STARTED here. + function testTransitionStateResetAfterFinishedToStartedTransition() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(uint8(details.state), uint8(LibCommon.CycleState.STARTED)); + assertEq(details.nextTaskIndexPosition, 0); + assertEq(details.expectedTasksToBeProcessed.length, 0); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + (,,, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.SUSPENDED)); + } + + /// @dev Proves the transition state stays clean through a full SUSPENDED->READY->STARTED + /// roundtrip (disable while tasks exist -> onCycleSuspend removes them -> READY -> re-enable). + function testTransitionStateResetAfterReadyPathRoundtrip() public { + registerUst(diamondAddr, 2450); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(1, taskIndexes); + + (,,, LibCommon.CycleState stateAfterSuspend) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfterSuspend), uint8(LibCommon.CycleState.READY)); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(details.nextTaskIndexPosition, 0); + assertEq(details.expectedTasksToBeProcessed.length, 0); + + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + (,,, LibCommon.CycleState stateAfterEnable) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfterEnable), uint8(LibCommon.CycleState.STARTED)); + + LibCommon.CycleDetails memory detailsAfterEnable = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(detailsAfterEnable.nextTaskIndexPosition, 0); + assertEq(detailsAfterEnable.expectedTasksToBeProcessed.length, 0); + } + + /// @dev Proves no staleness accumulates across repeated empty-registry fast-path cycle ends + /// (onCycleEndInternal's totalTasks()==0 branch, which skips the transition machinery entirely). + function testTransitionStateNoStalenessAcrossConsecutiveEmptyCycles() public { + for (uint256 i = 0; i < 3; i++) { + (, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (,,, LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(details.nextTaskIndexPosition, 0); + assertEq(details.expectedTasksToBeProcessed.length, 0); + } + } + + /// @dev Proves a populated cycle, followed by that task expiring/being removed in the next + /// transition, followed by a third cycle hitting the empty-registry fast path, leaves no + /// leftover staleness from the earlier populated transitions. + function testTransitionStateNoStalenessAfterPopulatedThenEmptyCycle() public { + registerUst(diamondAddr, 1_300); // expires during cycle 2 + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); // cycle 1 -> 2, task still active + + // Cycle 2 -> 3: task has now expired and gets dropped during this transition. + (uint64 indexBefore, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, taskIndexes); + vm.stopPrank(); + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + + // Cycle 3 -> 4: registry is now empty, hits the fast path. + (, uint64 start2, uint64 duration2,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start2 + duration2); + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (,,, LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(details.nextTaskIndexPosition, 0); + assertEq(details.expectedTasksToBeProcessed.length, 0); + } + + /// @dev Sanity ceiling on the gas cost of clearing a large expectedTasksToBeProcessed inside + /// moveToStartedState. If the redundant pre-reset line removed as part of this fix were ever + /// reintroduced, or the clearing became quadratic instead of linear, this would balloon far + /// past the ceiling asserted here. + function testLargeExpectedTasksListClearsWithoutGasRegression() public { + uint256 n = 150; + vm.deal(alice, n * 110 ether + 500 ether); + for (uint256 i = 0; i < n; i++) { + registerUst(diamondAddr, 2450); + } + + uint256[] memory taskIndexes = new uint256[](n); + for (uint256 i = 0; i < n; i++) { + taskIndexes[i] = i; + } + + (uint64 indexBefore, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + uint256 gasBefore = gasleft(); + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, taskIndexes); + uint256 gasUsed = gasBefore - gasleft(); + vm.stopPrank(); + + // Empirically confirmed: removing the redundant pre-reset line dropped this from + // 9,660,854 to 9,660,133 gas at N=150 -- strictly cheaper, never worse. + assertLt(gasUsed, 15_000_000); + } + + /// @notice Test to ensure config buffer is applied when no tasks exist during cycle end, updating the cycle duration directly. + function testConfigBufferAppliedWhenNoTasks() public { + ( , uint64 startBefore, uint64 durationBefore, ) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(durationBefore, 1200); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + 3600, 20_000_000, 0.5 ether, 1 ether, 50, 0.5 ether, 6, 6, 400, 2400, 3600, 20_000_000, 100 + ); + + vm.warp(startBefore + durationBefore); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 indexAfter, , uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexAfter, 2); + assertEq(durationAfter, 2400); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED)); + } + + /// @notice Test to ensure config buffer is applied after monitorCycleEnd + processTasks, resulting in STARTED state with the updated cycle duration. + function testCycleTransitionAppliesConfigBuffer() public { + registerUst(diamondAddr, 2450); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, ) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(durationBefore, 1200); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 6, 500, 2400, 3600, 5_000_000, 500 + ); + assertEq(IConfigFacet(diamondAddr).getConfigBuffer().cycleDurationSecs, 2400); + + vm.warp(startBefore + durationBefore); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + ICoreFacet(diamondAddr).processTasks(indexBefore + 1, tasks); + vm.stopPrank(); + + (uint64 indexAfter, , uint64 durationAfter, LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(indexAfter, indexBefore + 1); + assertEq(durationAfter, 2400); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED)); + } + + /// @notice Test to ensure 'processTasks' with an empty array returns early. + function testProcessTasksWithEmptyArrayReturnsEarly() public { + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory empty; + ICoreFacet(diamondAddr).processTasks(index + 1, empty); + vm.stopPrank(); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + } + + /// @notice Test to ensure that when buffer changes cycle duration, moveToReadyState resets transition state. + function testMoveToReadyStateResetsTransitionStateOnDurationChange() public { + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(duration, 1200); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateConfigBuffer( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 6, 500, 2400, 3600, 5_000_000, 500 + ); + + vm.warp(start + duration); + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.SUSPENDED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, tasks); + + (uint64 refundDuration, uint128 automationFeePerSec) = ICoreFacet(diamondAddr).getTransitionInfo(); + assertEq(refundDuration, 0); + assertEq(automationFeePerSec, 0); + + ( , , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.READY)); + } + + /// @notice Test to ensure partial task processing in FINISHED state keeps state FINISHED + /// until the last task is processed, then transitions to STARTED. + function testPartialTaskProcessingInFinishedState() public { + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + tasks[0] = 1; + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + vm.stopPrank(); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + } + + /// @notice Test to ensure partial task processing in SUSPENDED state keeps state SUSPENDED + /// until the last task is processed, then transitions to READY. + function testPartialTaskProcessingInSuspendedState() public { + registerUst(diamondAddr, 2450); + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.SUSPENDED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.startPrank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, tasks); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.SUSPENDED)); + + tasks[0] = 1; + ICoreFacet(diamondAddr).processTasks(index, tasks); + vm.stopPrank(); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.READY)); + } + + /// @notice Test to ensure an expired task is removed from the registry and 'RemovedTasks' is emitted during cycle transition. + function testExpiredTaskRemovalInTransition() public { + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.FINISHED)); + + // Task is in registry before expiration + assertEq(IRegistryFacet(diamondAddr).getTaskIdList().length, 1); + + // Move time forward past task expiration + vm.warp(block.timestamp + 1250); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + // Expect RemovedTasks event for the expired task + uint64[] memory expectedRemoved = new uint64[](1); + expectedRemoved[0] = 0; + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.RemovedTasks(expectedRemoved); + + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + vm.stopPrank(); + + // Task is removed from registry + assertEq(IRegistryFacet(diamondAddr).getTaskIdList().length, 0); + + ( , , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED)); + } + + /// @dev Test to ensure dropOrChargeTask's cancelled/expired-UST branch still removes the task + /// and proceeds without reverting when the registry's balance is insufficient to pay the + /// deposit refund, by design. + function testExpiredTaskRemovalInTransitionProceedsIfInsufficientBalanceForDepositRefund() public { + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + // Drain the registry's balance so the deposit refund cannot be paid. + uint256 diamondBalance = erc20Supra.balanceOf(diamondAddr); + vm.stopPrank(); + vm.prank(diamondAddr); + erc20Supra.transfer(bob, diamondBalance); + assertEq(erc20Supra.balanceOf(diamondAddr), 0); + + // Move time forward past task expiration. + vm.warp(block.timestamp + 1250); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectEmit(true, true, true, true); + emit IRegistryFacet.ErrorInsufficientBalanceToRefund(0, alice, 0, 60.1 ether); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + + // Task is still removed despite the failed refund -- cycle transition is not blocked. + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @notice Test to ensure a task is removed during transition when the owner does not have enough + /// allowance for the automation fee. The deposit is unlocked and forfeited to the registry. + function testInsufficientAllowanceDuringTransitionRemovesTask() public { + registerUst(diamondAddr, 2450); + uint256 balanceBefore = erc20Supra.balanceOf(alice); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + + // Revoke alice's allowance for the AutomationRegistry + vm.prank(alice); + erc20Supra.approve(diamondAddr, 0); + + ( , uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.FINISHED)); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.expectEmit(true, true, true, true); + emit ICoreFacet.TaskCancelledInsufficentBalanceAllowance(0, alice, 3 ether, 38.9 ether, 0, keccak256("txHash")); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(erc20Supra.balanceOf(alice), balanceBefore); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + } + + /// @notice Test to ensure enabling automation during SUSPENDED state makes the finalised transition go to STARTED. + function testEnableAutomationDuringSuspendedFinalizesToStarted() public { + registerUst(diamondAddr, 2450); + + (uint64 index, uint64 start, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateBefore), uint8(LibCommon.CycleState.SUSPENDED)); + + vm.prank(admin); + ICoreFacet(diamondAddr).enableAutomation(); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, tasks); + + ( , , , LibCommon.CycleState stateAfter) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED)); + } + + /// @dev refundTaskFees refunds the full cycle locked fee when the task's active timeframe + /// spans beyond the refund duration. With 2450s expiry, taskActiveTimeframe=1250s which + /// exceeds refundDuration=1200s, so actualFeeTimeframe is capped at 1200s (full cycle). + /// Result: all 3 ether locked fee is refunded + function testRefundTaskFeesOnSuspendForActiveTaskRefundsFullCycleFees() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + // State before refund + assertEq(erc20Supra.balanceOf(alice), 35.9 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.SUSPENDED)); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, taskIndexes); + + // State after refund + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(erc20Supra.balanceOf(alice), 99 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + } + + /// @dev refundTaskFees refunds only a partial locked fee when the task expires early in + /// the cycle. With 1250s expiry, taskActiveTimeframe=50s which is less than + /// refundDuration=1200s, so actualFeeTimeframe=50s. Only the fee for 50s (0.125 ether) is refunded. + function testRefundTaskFeesOnSuspendForActiveTaskRefundsPartialCycleFees() public { + registerUst(diamondAddr, 1250); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + (uint64 index, , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + // State before refund + assertEq(erc20Supra.balanceOf(alice), 35.9 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.SUSPENDED)); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, taskIndexes); + + // State after refund: only 0.125 ether of the 3 ether locked fee is refunded + // (50s worth out of 1200s cycle). + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(erc20Supra.balanceOf(alice), 96.125 ether); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + } + + /// @notice Test to ensure safeRefund emits ErrorInsufficientBalanceToRefund when the registry's balance is insufficient + /// to process refund, and that the task is still removed. + function testSafeRefundEmitsErrorInsufficientBalanceToRefundIfInsufficientBalance() public { + registerUst(diamondAddr, 1250); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + uint256 diamondBalance = erc20Supra.balanceOf(diamondAddr); + vm.prank(diamondAddr); + erc20Supra.transfer(bob, diamondBalance); + assertEq(erc20Supra.balanceOf(diamondAddr), 0); + + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + ( , , , LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.SUSPENDED)); + + vm.expectEmit(true, true, true, true); + emit IRegistryFacet.ErrorInsufficientBalanceToRefund(0, alice, 1, 0.125 ether); + + vm.expectEmit(true, true, true, true); + emit IRegistryFacet.ErrorInsufficientBalanceToRefund(0, alice, 0, 60.1 ether); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(2, taskIndexes); + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure the congestion fee uses the proportional surplus formula when + /// threshold usage exceeds congestion threshold percentage but not 100%. + function testRegisterWhenGasOccupancyIsAboveThresholdButBelowFullCapacity() public { + uint256 depositAmount = 451 ether; + + // Congestion multiplier is inactive below threshold + uint128 baseMultiplier = IRegistryFacet(diamondAddr).calculateAutomationFeeMultiplierForCommittedOccupancy(100_000); + assertEq(baseMultiplier, 0.5 ether, "sub-threshold should use base fee only"); + + // Congestion multiplier activates above 50% + uint128 congestedMultiplier = IRegistryFacet(diamondAddr).calculateAutomationFeeMultiplierForCommittedOccupancy(11_000_000); + assertEq(congestedMultiplier, 0.67004782 ether, "congestion should raise fee above base"); + + // Estimated fee + uint128 estimatedFee = IRegistryFacet(diamondAddr).estimateAutomationFee(11_000_000); + assertEq(estimatedFee, 442.2315612 ether); + + vm.startPrank(alice); + vm.deal(alice, depositAmount); + erc20SupraHandler.deposit{value: depositAmount}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + + uint128 cap = 450 ether; + + IRegistryFacet(diamondAddr).register( + createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, uint128(100))), + createPredicate(diamondAddr), + uint64(block.timestamp + 2450), + uint128(11_000_000), + uint128(4 gwei), + uint128(cap), + 0, + new bytes[](0) + ); + vm.stopPrank(); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), cap, "automation fee not deposited"); + assertEq(erc20Supra.balanceOf(alice), 0, "balance should be 0 after spending all [450 ether + 1 ether as flat reg fee]"); + } + + /// @dev Test to ensure registration succeeds when automation and congestion base fees are zero. + /// The fee multiplier and estimated fee are both 0 regardless of gas occupancy. + /// Only the flat registration fee and the user's cap are deducted from the balance. + function testRegisterWithZeroBaseFee() public { + address customRegistry = deployCustomRegistry(); + + // With zero base fees, multiplier and estimated fee should be 0 + uint128 multiplier = IRegistryFacet(customRegistry).calculateAutomationFeeMultiplierForCommittedOccupancy(100_000); + assertEq(multiplier, 0); + uint128 estimatedFee = IRegistryFacet(customRegistry).estimateAutomationFee(100_000); + assertEq(estimatedFee, 0); + + registerUst(customRegistry, 2450); + + assertTrue(IRegistryFacet(customRegistry).ifTaskExists(0)); + // Only flat reg fee(1 ether) and automation fee cap(60.1 ether) is deducted since estimated automation fee is 0 + assertEq(erc20Supra.balanceOf(alice), 38.9 ether); + } + + /// @dev Test to ensure calculateTaskFee returns 0 when the automationBaseFeeWeiPerSec is 0. + /// This is exercised during a cycle transition: calculateTaskFee exits early with 0 fee, + /// no cycle fees are locked, and the task activates normally. + function testCalculateTaskFeeReturnsZeroWhenBaseFeeIsZero() public { + address customRegistry = deployCustomRegistry(); + registerUst(customRegistry, 2450); + + // Perform cycle transition + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + processCycleTransition(customRegistry, tasks); + + // With zero fee, the cycle locked fee should be 0 + assertEq(IRegistryFacet(customRegistry).getCycleLockedFees(), 0); + + // Task should be active + assertTrue(IRegistryFacet(customRegistry).ifTaskExists(0)); + assertEq(IRegistryFacet(customRegistry).getActiveTaskIds().length, 1); + assertEq(IRegistryFacet(customRegistry).getActiveTaskIds()[0], 0); + } + + /// @notice Correctness test for LibCore.buildAliveOrderedTaskIds: registers tasks, + /// cancels one (creating a tombstone in RegistryState.orderedTaskIds), registers + /// another after that, then asserts monitorCycleEnd's resulting + /// expectedTasksToBeProcessed is exactly the ascending list of survivors — proving + /// the tombstoned entry is filtered out and ascending order is preserved. + function testBuildAliveOrderedTaskIdsFiltersCancelledAndStaysAscending() public { + registerUst(diamondAddr, 2450); // task 0 + registerUst(diamondAddr, 2450); // task 1 + registerUst(diamondAddr, 2450); // task 2 + + uint64[] memory toCancel = new uint64[](1); + toCancel[0] = 1; + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(toCancel); + + registerUst(diamondAddr, 2450); // task 3, registered after the cancellation + + (, uint64 start, uint64 duration,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + LibCommon.CycleDetails memory details = ICoreFacet(diamondAddr).getCycleStateDetails(); + assertEq(details.expectedTasksToBeProcessed.length, 3); + assertEq(details.expectedTasksToBeProcessed[0], 0); + assertEq(details.expectedTasksToBeProcessed[1], 2); + assertEq(details.expectedTasksToBeProcessed[2], 3); + } + + /// @notice Bounded-cost check for LibCommon.removeFromActiveTaskIds: RegistryState.activeTaskIds + /// is a plain uint256[], so removing from it is an O(k) linear-scan swap-remove + /// (k = current activeTaskIds length) rather than EnumerableSet's O(1). A single + /// stopTasks call spanning activeTaskIds' full length is therefore O(k^2), bounded by + /// the governance-owned taskCapacity+sysTaskCapacity cap (200 by default), not unbounded. + /// This registers 100 UST tasks (half the default UST cap), activates them all via a + /// single cycle transition, then stops them all in one call and asserts it completes + /// well within a realistic block gas limit — the bound is real, not just asymptotic. + function testStopTasksBulkRemovalFromActiveTaskIdsStaysWithinGasBudget() public { + uint256 n = 100; + uint256[] memory taskIndexes = new uint256[](n); + vm.deal(alice, n * 101 ether); + for (uint256 i = 0; i < n; i++) { + registerUst(diamondAddr, 2450); + taskIndexes[i] = i; + } + + processCycleTransition(diamondAddr, taskIndexes); + assertEq(IRegistryFacet(diamondAddr).getTotalActiveTasks(), n); + + uint64[] memory taskIndexesU64 = new uint64[](n); + for (uint256 i = 0; i < n; i++) { + taskIndexesU64[i] = uint64(i); + } + + vm.prank(alice); + uint256 gasBefore = gasleft(); + IRegistryFacet(diamondAddr).stopTasks(taskIndexesU64); + uint256 gasUsed = gasBefore - gasleft(); + + assertEq(IRegistryFacet(diamondAddr).getTotalActiveTasks(), 0); + // 30M is a representative mainnet-scale block gas limit; the worst-case O(k^2) + // bound at k=100 (~10,000 shift operations) sits far below it in practice. + assertLt(gasUsed, 30_000_000, "bulk stopTasks removal from activeTaskIds exceeded a realistic block gas limit"); + } +} diff --git a/solidity/supra_contracts/test/Counter.sol b/solidity/supra_contracts/test/Counter.sol new file mode 100644 index 0000000000..768cf86dc3 --- /dev/null +++ b/solidity/supra_contracts/test/Counter.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract Counter is OwnableUpgradeable, UUPSUpgradeable { + uint256 public counter; + address public privilegedAddress; + + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the owner and privileged address of the contract. + /// @param _privileged Privileged address. + function initialize(address _privileged) public initializer { + privilegedAddress = _privileged; + __Ownable_init(msg.sender); + } + + /// @notice Increments the counter by 1. + function increment() external { + if (msg.sender == privilegedAddress) { + counter = counter + 1; + } + } + + /// @notice Returns true if the counter is not divisible by 3, false otherwise. + /// Used during testing register automation task with condition "counter is not divisible by 3". + function isNotDivisibleBy3() external view returns (bool) { + return counter % 3 != 0; + } + + /// @notice Updates the counter to a new value. + /// @param newValue New value for the counter. + /// Used during testing to register trigger automation task execution by making isNotDivisibleBy3 condition to be true. + function update(uint256 newValue) external { + if (msg.sender == privilegedAddress) { + counter = newValue; + } + } + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/test/CycleTransitionGas.t.sol b/solidity/supra_contracts/test/CycleTransitionGas.t.sol new file mode 100644 index 0000000000..151ed9985d --- /dev/null +++ b/solidity/supra_contracts/test/CycleTransitionGas.t.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {console} from "forge-std/console.sol"; +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; + +/// @notice Gas benchmark for the automation-registry's three cycle-transition flows, +/// by default at the production task-registry cap (200 tasks: 160 UST + 40 +/// GST, matching LibDiamondUtils.defaultInitParams()), submitted as batches +/// of 25 tasks (the assumed VM_SIGNER batch size - not an on-chain constant). +/// +/// UST/GST task counts are overridable via env vars for one-off custom-N +/// runs, without touching this file - see +/// AUTOMATION_REGISTRY_GAS_GUIDE.md's "Running with a custom task count" +/// section: +/// CYCLE_GAS_BENCH_UST_COUNT (default 160) +/// CYCLE_GAS_BENCH_GST_COUNT (default 40) +/// CYCLE_GAS_BENCH_EXPIRING_UST_COUNT (default 20, scenario 3 only) +/// +/// 1. FINISHED -> STARTED, everything survives (testCycleTransitionGas_FullFlow_ProductionMix) +/// 2. STARTED (mid-cycle) -> SUSPENDED -> READY, everything is dropped/refunded +/// (testCycleTransitionGas_MidCycle_StartedToSuspended_FullRegistry) +/// 3. FINISHED -> STARTED with a subset of tasks expiring mid-transition +/// (testCycleTransitionGas_FullFlow_SecondCycle_WithExpiredTasks) +/// +/// `MonitorCycleEndGas.t.sol` only measures `monitorCycleEnd`. The order-independent +/// task-list compaction work (see Issue-3445) introduced costs that actually land in +/// `processTasks`, not `monitorCycleEnd`: +/// - Every surviving task pushes onto `transitionState.survivedTaskIds` +/// (LibCore.sol, dropOrChargeTasks) - a fresh-slot SSTORE per task, repeated in +/// whichever batch it falls into. +/// - The batch that finalizes the transition additionally pays +/// `updateRegistryState`'s two O(n) array assignments (`activeTaskIds`/ +/// `orderedTaskIds`) plus `moveToStartedState`'s `delete` of the whole transition +/// struct - costs proportional to the total task count, landed entirely on that +/// one terminal call, regardless of which of the three flows above it belongs to. +/// +/// Every measurement is logged so `forge test --match-contract CycleTransitionGasTest +/// -vv` gives a ready-to-read reference, the same way MonitorCycleEndGas.t.sol does. +/// The summary across all three scenarios is written up as a downstream-facing +/// reference in crates/supra-extension/src/AUTOMATION_REGISTRY_GAS_GUIDE.md. +contract CycleTransitionGasTest is BaseDiamondTest { + + /// @dev Default UST/GST counts when CYCLE_GAS_BENCH_UST_COUNT/ + /// CYCLE_GAS_BENCH_GST_COUNT are unset: taskCapacity=160 + sysTaskCapacity=40 + /// = 200, matching MAX_SUPPORTED_AUTOMATION_TASKS (crates/supra-extension) + /// and LibDiamondUtils.defaultInitParams(). Read via _ustTaskCount()/ + /// _gstTaskCount() rather than directly, everywhere except _deployRegistry's + /// "does this custom count still fit the production capacity" comparison. + uint256 constant TOTAL_UST = 160; + uint256 constant TOTAL_GST = 40; + + /// @dev Assumed per-processTasks-call batch size. Not an on-chain constant - + /// no such cap exists in the contract or in crates/supra-extension; this is + /// the off-chain VM_SIGNER submitter's convention being characterized here. + /// Not overridable - only the task counts are, per the class's NatSpec. + uint256 constant BATCH_SIZE = 25; + + /// @dev Generic single-tx sanity ceiling, matching the pattern already used in + /// CoreFacet.t.sol's testStopTasksBulkRemovalFromActiveTaskIdsStaysWithinGasBudget. + /// This is NOT the authoritative node-assigned processTasks record gas limit - + /// that calibration lives outside this repo. Confirming real headroom means + /// comparing the logged final-batch figure against whatever gas_limit the + /// node actually assigns to a processTasks record. + uint256 constant SANITY_GAS_CEILING = 30_000_000; + + // ──────────────────────────────────────────────────────────────────────── + // Helpers + // ──────────────────────────────────────────────────────────────────────── + + /// @dev Reads CYCLE_GAS_BENCH_UST_COUNT, falling back to the production default + /// (TOTAL_UST=160) when unset. + function _ustTaskCount() internal view returns (uint256) { + return vm.envOr("CYCLE_GAS_BENCH_UST_COUNT", TOTAL_UST); + } + + /// @dev Reads CYCLE_GAS_BENCH_GST_COUNT, falling back to the production default + /// (TOTAL_GST=40) when unset. + function _gstTaskCount() internal view returns (uint256) { + return vm.envOr("CYCLE_GAS_BENCH_GST_COUNT", TOTAL_GST); + } + + /// @dev Reads CYCLE_GAS_BENCH_EXPIRING_UST_COUNT, falling back to the default + /// (EXPIRING_UST_COUNT=20) when unset. Scenario 3 only. + function _expiringUstTaskCount() internal view returns (uint256) { + return vm.envOr("CYCLE_GAS_BENCH_EXPIRING_UST_COUNT", EXPIRING_UST_COUNT); + } + + /// @dev Deploys a diamond sized for `_ustCount` UST + `_gstCount` GST tasks. + /// When both counts fit the production defaults (LibDiamondUtils. + /// defaultInitParams' taskCapacity=160/sysTaskCapacity=40), InitParams are + /// left completely unmodified, so the default (no env vars set) run is + /// byte-for-byte the same deployment as before this was made configurable. + /// A count exceeding its production default widens that capacity - and its + /// paired gas cap, so registration isn't gated by it - to fit, following the + /// same `n * 100_000 + 1_000_000` sizing MonitorCycleEndGas.t.sol uses. + /// Also authorizes `bob` to submit GST tasks. + function _deployRegistry(uint256 _ustCount, uint256 _gstCount) internal returns (address diamond) { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + + if (_ustCount > p.taskCapacity) { + p.taskCapacity = uint16(_ustCount); + p.registryMaxGasCap = uint128(_ustCount * 100_000 + 1_000_000); + } + if (_gstCount > p.sysTaskCapacity) { + p.sysTaskCapacity = uint16(_gstCount); + p.sysRegistryMaxGasCap = uint128(_gstCount * 100_000 + 1_000_000); + } + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + diamond = d.diamond; + IConfigFacet(diamond).grantAuthorization(bob); + vm.stopPrank(); + } + + /// @dev Registers `_n` UST tasks on `_diamond` with an explicit `_expiry`, + /// bulk-funding alice once upfront. Unlike MonitorCycleEndGas.t.sol's + /// registration helper (which only measures monitorCycleEnd and never runs + /// processTasks), this benchmark also drives every task through + /// dropOrChargeTask -> tryWithdrawTaskAutomationFee, which pulls an + /// *additional* per-cycle automation fee from alice on top of the 61.1 + /// ether/task locked at registration (flatRegistrationFeeWei 1 ether + + /// automationFeeCapForCycle 60.1 ether). At production defaults that fee is + /// ~3 ether/task (automationBaseFeeWeiPerSec 0.5 ether/sec * cycleDurationSecs + /// 1200s * maxGasAmount 100_000 / registryMaxGasCap 20_000_000). 200 ether/task + /// leaves a large margin over the ~64.1 ether/task actually required. + /// @param _expiry Absolute expiry timestamp. Must be strictly greater than the + /// current cycle's end time (LibRegistry.validateTaskDuration enforces this + /// at registration) - callers that want a task to expire mid-registry-lifetime + /// must register it with an expiry inside a *later* cycle, then let time pass. + function _registerUstTasks(address _diamond, uint256 _n, uint64 _expiry) internal { + uint256 depositAmount = _n * 200 ether; + vm.deal(alice, depositAmount + 100 ether); + + bytes[] memory auxData; + bytes memory payload = createPayload( + 0, + address(erc20SupraHandler), + abi.encodeCall(erc20SupraHandler.withdraw, 100) + ); + bytes memory predicate = createPredicate(_diamond); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: depositAmount}(); + erc20Supra.approve(_diamond, type(uint256).max); + + for (uint256 i = 0; i < _n; i++) { + IRegistryFacet(_diamond).register( + payload, + predicate, + _expiry, + uint128(100_000), // maxGasAmount + uint128(4 gwei), // gasPriceCap + uint128(60.1 ether), // automationFeeCapForCycle + 2, // priority + auxData + ); + } + vm.stopPrank(); + } + + /// @dev Registers `_n` GST (system) tasks on `_diamond` as `bob`. GST tasks are not + /// charged (see LibCore.dropOrChargeTask's GST branch), so no funding is needed. + function _registerGstTasks(address _diamond, uint256 _n) internal { + bytes[] memory auxData; + bytes memory payload = createPayload( + 0, + address(erc20SupraHandler), + abi.encodeCall(erc20SupraHandler.withdraw, 100) + ); + bytes memory predicate = createPredicate(_diamond); + uint64 expiry = uint64(block.timestamp + 86400); + + for (uint256 i = 0; i < _n; i++) { + vm.prank(bob); + IRegistryFacet(_diamond).registerSystemTask( + payload, + predicate, + expiry, + uint128(100_000), // maxGasAmount + 2, // priority + auxData + ); + } + } + + /// @dev Warps to the cycle boundary and measures monitorCycleEnd's gas. + /// gasleft() brackets (not vm.pauseGasMetering) so SLOAD/SSTORE costs are + /// captured faithfully - see MonitorCycleEndGas.t.sol's identical rationale. + function _measureMonitorCycleEnd(address _diamond) internal returns (uint256 gasUsed) { + (, uint64 startTime, uint64 durationSecs,) = ICoreFacet(_diamond).getCycleInfo(); + vm.warp(startTime + durationSecs); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + + uint256 before = gasleft(); + ICoreFacet(_diamond).monitorCycleEnd(); + gasUsed = before - gasleft(); + } + + /// @dev Measures a single processTasks batch's gas. processTasks checks + /// msg.sender only (unlike monitorCycleEnd's tx.origin check), so a + /// single-arg prank is enough. + function _measureProcessTasksBatch( + address _diamond, + uint64 _cycleIndex, + uint256[] memory _indexes + ) internal returns (uint256 gasUsed) { + vm.prank(LibUtils.VM_SIGNER); + + uint256 before = gasleft(); + ICoreFacet(_diamond).processTasks(_cycleIndex, _indexes); + gasUsed = before - gasleft(); + } + + /// @dev Builds the contiguous range of task indexes `[_start, _start + _count)`. + function _buildRangeIndexes(uint256 _start, uint256 _count) internal pure returns (uint256[] memory indexes) { + indexes = new uint256[](_count); + for (uint256 i = 0; i < _count; i++) { + indexes[i] = _start + i; + } + } + + /// @dev Submits all of `_taskCount` registered tasks to `_diamond` for + /// `_cycleIndex` as contiguous batches of BATCH_SIZE, logging and returning + /// each batch's gas. `_taskCount` need not be a multiple of BATCH_SIZE - the + /// last batch is simply smaller (custom task counts aren't guaranteed to + /// divide evenly). + function _runAllBatches( + address _diamond, + uint64 _cycleIndex, + uint256 _taskCount, + string memory _logPrefix + ) internal returns (uint256[] memory batchGas) { + uint256 numBatches = (_taskCount + BATCH_SIZE - 1) / BATCH_SIZE; + batchGas = new uint256[](numBatches); + uint256 processed; + for (uint256 b = 0; b < numBatches; b++) { + uint256 remaining = _taskCount - processed; + uint256 count = remaining < BATCH_SIZE ? remaining : BATCH_SIZE; + batchGas[b] = _measureProcessTasksBatch(_diamond, _cycleIndex, _buildRangeIndexes(processed, count)); + console.log(string.concat(_logPrefix, vm.toString(b + 1), " |"), batchGas[b]); + processed += count; + } + } + + /// @dev Summarizes and logs a scenario's gas figures: the triggering call + /// (monitorCycleEnd or disableAutomation) plus all processTasks batches, + /// isolating the final batch's finalization premium over a typical batch. + /// Returns the final batch's gas so callers can assert a sanity ceiling on it. + function _summarizeAndLog( + string memory _scenarioLabel, + uint256 _triggerGas, + uint256[] memory _batchGas + ) internal pure returns (uint256 finalBatchGas) { + uint256 numBatches = _batchGas.length; + uint256 totalBatchGas; + uint256 nonFinalTotal; + for (uint256 b = 0; b < numBatches; b++) { + totalBatchGas += _batchGas[b]; + if (b < numBatches - 1) { + nonFinalTotal += _batchGas[b]; + } + } + uint256 nonFinalAverage = numBatches > 1 ? nonFinalTotal / (numBatches - 1) : 0; + finalBatchGas = _batchGas[numBatches - 1]; + uint256 finalizationPremium = finalBatchGas > nonFinalAverage ? finalBatchGas - nonFinalAverage : 0; + + console.log(string.concat("=== ", _scenarioLabel, " ===")); + console.log("trigger-call gas (monitorCycleEnd/disableAutomation) :", _triggerGas); + console.log("total processTasks gas (all batches) :", totalBatchGas); + console.log("non-final batch average gas :", nonFinalAverage); + console.log("final batch gas :", finalBatchGas); + console.log("final-batch finalization premium :", finalizationPremium); + console.log("grand total (trigger + all processTasks batches) :", _triggerGas + totalBatchGas); + } + + // ──────────────────────────────────────────────────────────────────────── + // Scenario 1: FINISHED -> STARTED, full registry, everything survives + // ──────────────────────────────────────────────────────────────────────── + + function testCycleTransitionGas_FullFlow_ProductionMix() public { + uint256 ustCount = _ustTaskCount(); + uint256 gstCount = _gstTaskCount(); + uint256 totalTasks = ustCount + gstCount; + + address diamond = _deployRegistry(ustCount, gstCount); + _registerUstTasks(diamond, ustCount, uint64(block.timestamp + 86400)); + _registerGstTasks(diamond, gstCount); + + (uint64 cycleIndexBefore, , , ) = ICoreFacet(diamond).getCycleInfo(); + + uint256 monitorGas = _measureMonitorCycleEnd(diamond); + + (, , , LibCommon.CycleState stateAfterMonitor) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfterMonitor), uint8(LibCommon.CycleState.FINISHED), "cycle must be FINISHED after monitorCycleEnd"); + + uint256[] memory batchGas = _runAllBatches( + diamond, + cycleIndexBefore + 1, + totalTasks, + "processTasks gas | scenario 1, batch=" + ); + + (uint64 cycleIndexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED), "cycle must be STARTED after full transition"); + assertEq(cycleIndexAfter, cycleIndexBefore + 1, "cycle index must increment exactly once"); + assertEq(IRegistryFacet(diamond).getActiveTaskIds().length, totalTasks, "all tasks must survive the transition"); + + uint256 finalBatchGas = _summarizeAndLog( + string.concat("Scenario 1: FINISHED->STARTED, N=", vm.toString(totalTasks), ", all survive"), + monitorGas, + batchGas + ); + + // Generic single-tx sanity ceiling only - see SANITY_GAS_CEILING's NatSpec. + // A large enough custom task count can legitimately exceed this - raise it + // (or read the logged figures directly) rather than treating a failure here + // as a contract bug. + assertLt(finalBatchGas, SANITY_GAS_CEILING, "final batch gas exceeds generic sanity ceiling"); + } + + // ──────────────────────────────────────────────────────────────────────── + // Scenario 2: STARTED (mid-cycle) -> SUSPENDED -> READY, full registry, everything + // is dropped and refunded. + // ──────────────────────────────────────────────────────────────────────── + + function testCycleTransitionGas_MidCycle_StartedToSuspended_FullRegistry() public { + uint256 ustCount = _ustTaskCount(); + uint256 gstCount = _gstTaskCount(); + uint256 totalTasks = ustCount + gstCount; + + address diamond = _deployRegistry(ustCount, gstCount); + _registerUstTasks(diamond, ustCount, uint64(block.timestamp + 86400)); + _registerGstTasks(diamond, gstCount); + + (uint64 cycleIndex, uint64 startTime, uint64 durationSecs, ) = ICoreFacet(diamond).getCycleInfo(); + // "Middle of the cycle": halfway to cycleEndTime, well before it - disableAutomation's + // STARTED-branch (LibCore.tryMoveToSuspendedState) reverts if currentTime >= cycleEndTime. + vm.warp(startTime + durationSecs / 2); + + vm.prank(admin); + uint256 before = gasleft(); + ICoreFacet(diamond).disableAutomation(); + uint256 suspendTriggerGas = before - gasleft(); + + (, , , LibCommon.CycleState stateAfterDisable) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfterDisable), uint8(LibCommon.CycleState.SUSPENDED), "cycle must be SUSPENDED after mid-cycle disableAutomation"); + + // onCycleSuspend checks s.index == _cycleIndex (unlike onCycleTransition's + // index+1 - suspension does not increment the cycle index). + uint256[] memory batchGas = _runAllBatches( + diamond, + cycleIndex, + totalTasks, + "processTasks (onCycleSuspend) gas | scenario 2, batch=" + ); + + (uint64 cycleIndexAfter, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.READY), "cycle must be READY once suspension finalizes with automation disabled"); + assertEq(cycleIndexAfter, cycleIndex, "cycle index must NOT change on suspension - only STARTED transitions increment it"); + assertEq(IRegistryFacet(diamond).getActiveTaskIds().length, 0, "no tasks survive a suspension - all refunded and removed"); + assertEq(IRegistryFacet(diamond).totalTasks(), 0, "registry must be empty after full suspension"); + + uint256 finalBatchGas = _summarizeAndLog( + string.concat("Scenario 2: STARTED(mid-cycle)->SUSPENDED->READY, N=", vm.toString(totalTasks), ", all dropped"), + suspendTriggerGas, + batchGas + ); + + assertLt(finalBatchGas, SANITY_GAS_CEILING, "final batch gas exceeds generic sanity ceiling"); + } + + // ──────────────────────────────────────────────────────────────────────── + // Scenario 3: FINISHED -> STARTED where a subset of tasks expire mid-transition. + // + // LibRegistry.validateTaskDuration rejects any registration whose expiry falls + // at or before the *current* cycle's end time, so a task can never already be + // expired at the very first transition it survives into. To get genuinely + // expiring tasks, this test registers EXPIRING_UST_COUNT tasks with an expiry + // that falls inside cycle 2 (so they survive cycle 1's transition, since + // expiry > cycle1EndTime), then runs a second cycle transition where those same + // tasks are now past expiry (cycle2EndTime >= expiry) and get dropped instead of + // renewed. Cycle 1's transition is just setup here; cycle 2's is the measured one. + // ──────────────────────────────────────────────────────────────────────── + + /// @dev Default number of the UST tasks that expire mid-way, when + /// CYCLE_GAS_BENCH_EXPIRING_UST_COUNT is unset. + uint256 constant EXPIRING_UST_COUNT = 20; + + function testCycleTransitionGas_FullFlow_SecondCycle_WithExpiredTasks() public { + uint256 ustCount = _ustTaskCount(); + uint256 gstCount = _gstTaskCount(); + uint256 totalTasks = ustCount + gstCount; + uint256 expiringCount = _expiringUstTaskCount(); + require(expiringCount <= ustCount, "CYCLE_GAS_BENCH_EXPIRING_UST_COUNT must not exceed the UST task count"); + + address diamond = _deployRegistry(ustCount, gstCount); + + (, uint64 startTime1, uint64 durationSecs1, ) = ICoreFacet(diamond).getCycleInfo(); + uint64 cycle1EndTime = startTime1 + durationSecs1; + // Falls inside cycle 2 (cycle2EndTime = cycle1EndTime + durationSecs1, and this + // is well short of that), so registration passes but the task is expired by + // the time cycle 2 ends. + uint64 expiringExpiry = cycle1EndTime + 300; + + _registerUstTasks(diamond, expiringCount, expiringExpiry); + _registerUstTasks(diamond, ustCount - expiringCount, uint64(block.timestamp + 86400)); + _registerGstTasks(diamond, gstCount); + + // ---- Cycle 1 transition: nothing has expired yet, everything survives. ---- + (uint64 cycleIndex1, , , ) = ICoreFacet(diamond).getCycleInfo(); + _measureMonitorCycleEnd(diamond); + _runAllBatches(diamond, cycleIndex1 + 1, totalTasks, "processTasks gas | scenario 3, cycle 1 setup, batch="); + assertEq(IRegistryFacet(diamond).getActiveTaskIds().length, totalTasks, "cycle 1: nothing expired yet, all tasks survive"); + + // ---- Cycle 2 transition: the expiringCount short-expiry UST tasks are now + // past their expiry and get dropped instead of renewed. ---- + (uint64 cycleIndex2, , , ) = ICoreFacet(diamond).getCycleInfo(); + uint256 monitorGas2 = _measureMonitorCycleEnd(diamond); + + (, , , LibCommon.CycleState stateAfterMonitor2) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfterMonitor2), uint8(LibCommon.CycleState.FINISHED), "cycle 2 must be FINISHED after monitorCycleEnd"); + + uint256[] memory batchGas = _runAllBatches( + diamond, + cycleIndex2 + 1, + totalTasks, + "processTasks gas | scenario 3, cycle 2, batch=" + ); + + (uint64 cycleIndex3, , , LibCommon.CycleState stateAfter) = ICoreFacet(diamond).getCycleInfo(); + assertEq(uint8(stateAfter), uint8(LibCommon.CycleState.STARTED), "cycle must be STARTED after cycle 2's transition"); + assertEq(cycleIndex3, cycleIndex2 + 1, "cycle index must increment exactly once"); + assertEq( + IRegistryFacet(diamond).getActiveTaskIds().length, + totalTasks - expiringCount, + "the expiring UST tasks must be dropped, the rest survive" + ); + + uint256 finalBatchGas = _summarizeAndLog( + string.concat( + "Scenario 3: FINISHED->STARTED (cycle 2), N=", vm.toString(totalTasks), + " with ", vm.toString(expiringCount), " expiring" + ), + monitorGas2, + batchGas + ); + + assertLt(finalBatchGas, SANITY_GAS_CEILING, "final batch gas exceeds generic sanity ceiling"); + } +} diff --git a/solidity/supra_contracts/test/DiamondInit.t.sol b/solidity/supra_contracts/test/DiamondInit.t.sol new file mode 100644 index 0000000000..9d0994afff --- /dev/null +++ b/solidity/supra_contracts/test/DiamondInit.t.sol @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {OwnershipFacet} from "../src/facets/OwnershipFacet.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibDiamond} from "../src/libraries/LibDiamond.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {Config} from "../src/libraries/LibAppStorage.sol"; +import {FacetsDeployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol"; +import {IDiamondLoupe} from "../src/interfaces/IDiamondLoupe.sol"; +import {IRegistryStatus} from "../src/interfaces/IRegistryStatus.sol"; +import {IERC173} from "../src/interfaces/IERC173.sol"; +import {IERC165} from "../src/interfaces/IERC165.sol"; +import {DiamondInit} from "../src/upgradeInitializers/DiamondInit.sol"; +import {Diamond} from "../src//Diamond.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +contract DiamondInitTest is BaseDiamondTest { + + /// @dev Test to ensure all state variables are initialized correctly. + function testInitialize() public view { + assertEq(OwnershipFacet(diamondAddr).owner(), admin); + + (uint64 index, uint64 startTime, uint64 durationSecs, LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(index, 1); + assertEq(startTime, block.timestamp); + assertEq(durationSecs, 1200); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + assertEq(IRegistryFacet(diamondAddr).getNextCycleRegistryMaxGasCap(), 20_000_000); + assertEq(IRegistryFacet(diamondAddr).getNextCycleSysRegistryMaxGasCap(), 20_000_000); + assertTrue(IConfigFacet(diamondAddr).isRegistrationEnabled()); + assertTrue(ICoreFacet(diamondAddr).isAutomationEnabled()); + assertEq(IConfigFacet(diamondAddr).erc20Supra(), address(erc20Supra)); + + Config memory config = IConfigFacet(diamondAddr).getConfig(); + + assertEq(config.registryMaxGasCap, 20_000_000); + assertEq(config.sysRegistryMaxGasCap, 20_000_000); + assertEq(config.automationBaseFeeWeiPerSec, 0.5 ether); + assertEq(config.flatRegistrationFeeWei, 1 ether); + assertEq(config.congestionBaseFeeWeiPerSec, 0.5 ether); + assertEq(config.taskDurationCapSecs, 3600 * 24 * 7); + assertEq(config.sysTaskDurationCapSecs, 3600 * 24 * 180); + assertEq(config.cycleDurationSecs, 1200); + assertEq(config.taskCapacity, 160); + assertEq(config.sysTaskCapacity, 40); + assertEq(config.congestionThresholdPercentage, 50); + assertEq(config.congestionExponent, 6); + assertEq(config.maxCongestionExponent, 6); + } + + /// @dev Test to ensure all interfaces are registered. + function testInterfacesRegistered() public view { + assertTrue(IERC165(diamondAddr).supportsInterface(type(IERC165).interfaceId)); + assertTrue(IERC165(diamondAddr).supportsInterface(type(IDiamondCut).interfaceId)); + assertTrue(IERC165(diamondAddr).supportsInterface(type(IDiamondLoupe).interfaceId)); + assertTrue(IERC165(diamondAddr).supportsInterface(type(IERC173).interfaceId)); + assertTrue(IERC165(diamondAddr).supportsInterface(type(IRegistryStatus).interfaceId)); + } + + /// @dev Test to ensure 'init' selector is not registered. + function testInitSelectorNotRegistered() public view { + address facet = IDiamondLoupe(diamondAddr).facetAddress(DiamondInit.init.selector); + assertEq(facet, address(0)); + } + + /// @dev Test to ensure Diamond reverts if 'init' is called. + function testInitReverts() public { + InitParams memory params = LibDiamondUtils.defaultInitParams(); + + vm.expectRevert(LibDiamond.FunctionDoesNotExist.selector); + + vm.prank(admin); + DiamondInit(diamondAddr).init( + params, + address(erc20Supra) + ); + } + + /// @dev Test to ensure 'init' can only ever run once (smr-moonshot#3451). + function testInitCannotBeReplayedViaDiamondCut() public { + IDiamondCut.FacetCut[] memory emptyCut = new IDiamondCut.FacetCut[](0); + bytes memory initCalldata = abi.encodeCall(DiamondInit.init, (defaultParams, address(erc20Supra))); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(emptyCut, deployment.facets.diamondInit, initCalldata); + } + + /// @dev Test to ensure a future upgrade initializer tagged `reinitializer(2)` can still + /// run successfully after DiamondInit.init() has already consumed Initializable's + /// version 1 in the Diamond's shared storage (smr-moonshot#3451). + function testReinitializerCanRunAfterInitialInitialization() public { + MockDiamondInitV2 v2Init = new MockDiamondInitV2(); + IDiamondCut.FacetCut[] memory emptyCut = new IDiamondCut.FacetCut[](0); + bytes memory initV2Calldata = abi.encodeCall(MockDiamondInitV2.initV2, ()); + + vm.expectEmit(false, false, false, false); + emit MockDiamondInitV2.ReinitializedV2(); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(emptyCut, address(v2Init), initV2Calldata); + } + + /// @dev Test to ensure Diamond reverts if native token is sent to it. + function testDiamondTxFailsIfNativeTokenIsSent() public { + vm.prank(alice); + (bool success, ) = diamondAddr.call{value: 1 ether}(""); + assertFalse(success); + } + + /// @dev Test to ensure Diamond reverts if an unknown selector is called. + function testUnknownSelectorReverts() public { + vm.expectRevert(LibDiamond.FunctionDoesNotExist.selector); + + INonExistent(diamondAddr).nonExistent(); + } + + /// @dev Test to ensure 'facetAddresses' returns the address of all the facets. + function testLoupeFacetAddresses() public view { + address[] memory facets = IDiamondLoupe(diamondAddr).facetAddresses(); + assertEq(facets.length, 6); // diamondCut, loupe, ownership, config, registry, core + + bool diamondCutExists; + bool loupeExists; + bool ownershipExists; + bool registryExists; + bool coreExists; + + for (uint i; i < facets.length; i++) { + if (facets[i] == deployment.facets.diamondCutFacet) diamondCutExists = true; + if (facets[i] == deployment.facets.loupeFacet) loupeExists = true; + if (facets[i] == deployment.facets.ownershipFacet) ownershipExists = true; + if (facets[i] == deployment.facets.registryFacet) registryExists = true; + if (facets[i] == deployment.facets.coreFacet) coreExists = true; + } + + assertTrue(diamondCutExists); + assertTrue(loupeExists); + assertTrue(ownershipExists); + assertTrue(registryExists); + assertTrue(coreExists); + } + + /// @dev Test to ensure 'facetAddress' points to correct facet for a selector. + function testSelectorRouting() public view { + assertEq( + IDiamondLoupe(diamondAddr).facetAddress(IRegistryFacet.register.selector), + deployment.facets.registryFacet + ); + + assertEq( + IDiamondLoupe(diamondAddr).facetAddress(ICoreFacet.enableAutomation.selector), + deployment.facets.coreFacet + ); + + assertEq( + IDiamondLoupe(diamondAddr).facetAddress(OwnershipFacet.transferOwnership.selector), + deployment.facets.ownershipFacet + ); + } + + /// @dev Test to ensure 'transferOwnership' transfers the ownership. + function testTransferOwnership() public { + vm.prank(admin); + OwnershipFacet(diamondAddr).transferOwnership(alice); + + assertEq(OwnershipFacet(diamondAddr).owner(), alice); + } + + /// @dev Test to ensure 'transferOwnership' reverts if caller is not owner. + function testTransferOwnershipRevertsIfNotOwner() public { + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + OwnershipFacet(diamondAddr).transferOwnership(bob); + } + + /// @dev Test to ensure 'transferOwnership' reverts if the new owner is the zero address + /// (renouncing ownership is not supported). + function testTransferOwnershipRevertsIfNewOwnerIsZero() public { + vm.expectRevert(LibDiamond.AddressCannotBeZero.selector); + + vm.prank(admin); + OwnershipFacet(diamondAddr).transferOwnership(address(0)); + } + + /// @dev Test to ensure 'diamondCut' reverts if caller is not owner. + function testDiamondCutRevertsIfNotOwner() public { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = IRegistryFacet.register.selector; + selectors[1] = IRegistryFacet.registerSystemTask.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: diamondAddr, + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.MustBeContractOwner.selector); + + vm.prank(alice); + IDiamondCut(diamondAddr).diamondCut( + cut, + address(0), + "" + ); + } + + /// @dev Test to ensure adding a selector works correctly. + function testAddSelector() public { + uint256 numFacetsBefore = IDiamondLoupe(diamondAddr).facetAddresses().length; + + // Deploy mock facet + MockRegistryFacet mockRegistryFacet = new MockRegistryFacet(); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRegistryFacet.counter.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(mockRegistryFacet), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectors + }); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + + assertEq(IDiamondLoupe(diamondAddr).facetAddress(MockRegistryFacet.counter.selector), address(mockRegistryFacet)); + assertEq(IDiamondLoupe(diamondAddr).facetAddresses().length , numFacetsBefore + 1); + + assertEq(MockRegistryFacet(diamondAddr).counter(), 1); + } + + /// @dev Test to ensure adding an existing selector reverts. + function testAddExistingSelectorReverts() public { + // Deploy mock facet + MockRegistryFacet mockRegistryFacet = new MockRegistryFacet(); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRegistryFacet.erc20Supra.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(mockRegistryFacet), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.FunctionAlreadyExists.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + } + + /// @dev Test to ensure 'diamondCut' reverts if empty array of selectors is passed as selectors to be added. + function testAddWithEmptySelectorsReverts() public { + bytes4[] memory selectors; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: deployment.facets.registryFacet, + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.NoSelectorsInFacetToCut.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + } + + /// @dev Test to ensure 'diamondCut' reverts if address(0) is passed as facet address. + function testAddSelectorWithZeroAddressReverts() public { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRegistryFacet.counter.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(0), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.AddressCannotBeZero.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + } + + /// @dev Test to ensure removing a selector works correclty. + function testRemoveSelector() public { + uint256 numSelectorsBefore = IDiamondLoupe(diamondAddr).facetFunctionSelectors(deployment.facets.registryFacet).length; + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IRegistryFacet.cancelTasks.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(0), + action: IDiamondCut.FacetCutAction.Remove, + functionSelectors: selectors + }); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + + // Verify selector mapping cleared + address facet = IDiamondLoupe(diamondAddr).facetAddress(IRegistryFacet.cancelTasks.selector); + assertEq(facet, address(0)); + + uint256 numSelectorsAfter = IDiamondLoupe(diamondAddr).facetFunctionSelectors(deployment.facets.registryFacet).length; + assertEq(numSelectorsAfter, numSelectorsBefore - 1); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + // Verify call now reverts + vm.expectRevert(LibDiamond.FunctionDoesNotExist.selector); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'diamondCut' reverts if tried to remove a selector that doesn't exist. + function testRemoveNonExistingSelectorReverts() public { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRegistryFacet.counter.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(0), + action: IDiamondCut.FacetCutAction.Remove, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.FunctionDoesNotExist.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + } + + /// @dev Test to ensure replacing a selector works correclty. + function testReplaceSelector() public { + // Deploy mock facet + MockRegistryFacet mockRegistryFacet = new MockRegistryFacet(); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IConfigFacet.erc20Supra.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(mockRegistryFacet), + action: IDiamondCut.FacetCutAction.Replace, + functionSelectors: selectors + }); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + + // Verify selector now points to mockRegistryFacet + address facet = IDiamondLoupe(diamondAddr).facetAddress(IConfigFacet.erc20Supra.selector); + assertEq(facet, address(mockRegistryFacet)); + + // Verify logic changed + assertEq(IConfigFacet(diamondAddr).erc20Supra(), address(0x999)); + } + + /// @dev Test to ensure replacing a selector with same facet address reverts. + function testReplaceWithSameFacetReverts() public { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IConfigFacet.erc20Supra.selector; + + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: deployment.facets.configFacet, + action: IDiamondCut.FacetCutAction.Replace, + functionSelectors: selectors + }); + + vm.expectRevert(LibDiamond.CannotReplaceFunctionWithSameFunction.selector); + + vm.prank(admin); + IDiamondCut(diamondAddr).diamondCut(cut, address(0), ""); + } + + /// @dev Test to ensure the constructor reverts if the genesis owner is the zero address. + function testInitializeRevertsIfOwnerIsZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + vm.expectRevert(LibDiamond.AddressCannotBeZero.selector); + new Diamond(address(0), facets, address(erc20Supra), defaultParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if ERC20Supra address is zero. + function testInitializeRevertsIfErc20SupraIsZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + // address(0) as ERC20Supra + new Diamond(admin, facets, address(0), defaultParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if EOA is passed as ERC20Supra address. + function testInitializeRevertsIfErc20SupraIsEoa() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + vm.expectRevert(LibUtils.AddressCannotBeEOA.selector); + // EOA address as ERC20Supra + new Diamond(admin, facets, admin, defaultParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if task duration is <= cycle duration. + function testInitializeRevertsIfInvalidTaskDuration() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 1200, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidTaskDuration.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if registry max gas cap is zero. + function testInitializeRevertsIfRegistryMaxGasCapZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 0, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidRegistryMaxGasCap.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if congestion threshold percentage is > 100. + function testInitializeRevertsIfInvalidCongestionThreshold() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 101, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidCongestionThreshold.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if congestion exponent is 0. + function testInitializeRevertsIfCongestionExponentZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 0, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidCongestionExponent.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if task capacity is 0. + function testInitializeRevertsIfTaskCapacityZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 0, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidTaskCapacity.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if cycle duration is 0. + function testInitializeRevertsIfCycleDurationZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 0, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidCycleDuration.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if system task duration is <= cycle duration. + function testInitializeRevertsIfInvalidSysTaskDuration() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 1200, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidSysTaskDuration.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if system registry max gas cap is 0. + function testInitializeRevertsIfSysRegistryMaxGasCapZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 0, + sysTaskCapacity: 100, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidSysRegistryMaxGasCap.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization fails if system task capacity is 0. + function testInitializeRevertsIfSysTaskCapacityZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deployFacets(); + InitParams memory initParams = InitParams({ + taskDurationCapSecs: 3600 * 24 * 7, + registryMaxGasCap: 20_000_000, + automationBaseFeeWeiPerSec: 0.5 ether, + flatRegistrationFeeWei: 1 ether, + congestionThresholdPercentage: 50, + congestionBaseFeeWeiPerSec: 0.5 ether, + congestionExponent: 6, + maxCongestionExponent: 6, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 0, + registrationEnabled: true, + automationEnabled: true + }); + + vm.expectRevert(LibCommon.InvalidSysTaskCapacity.selector); + new Diamond(admin, facets, address(erc20Supra), initParams); + vm.stopPrank(); + } + + /// @dev Test to ensure 'facets' returns all registered facets. + function testFacets() public view { + IDiamondLoupe.Facet[] memory facetList = IDiamondLoupe(diamondAddr).facets(); + assertEq(facetList.length, 6); + + for (uint256 i; i < facetList.length; i++) { + assertTrue(facetList[i].facetAddress != address(0)); + assertGt(facetList[i].functionSelectors.length, 0); + } + } + + /// @dev Test to ensure 'isInitialized' returns true after initialization, and that it is + /// routed through the diamond's normal selector table (not shadowed by Diamond.sol itself). + function testIsInitialized() public view { + assertTrue(IRegistryStatus(diamondAddr).isInitialized()); + assertEq( + IDiamondLoupe(diamondAddr).facetAddress(IRegistryStatus.isInitialized.selector), + deployment.facets.loupeFacet + ); + } + + /// @dev Test to ensure 'isInitialized' genuinely reads live storage + /// (LibDiamond.DiamondStorage.initialized) rather than being permanently true by construction alone. + function testIsInitializedReflectsDedicatedFlagDirectly() public { + assertTrue(IRegistryStatus(diamondAddr).isInitialized()); + + // DiamondStorage.initialized packs into relative slot 4, byte offset 20, alongside + // contractOwner (bytes 0-19 of the same slot) -- confirmed via `forge inspect + // storage-layout + // --json`, not hand-computed. Re-verify if DiamondStorage's field order changes. + bytes32 slot = bytes32(uint256(LibDiamond.DIAMOND_STORAGE_POSITION) + 4); + bytes32 currentValue = vm.load(diamondAddr, slot); + + // Clear only the `initialized` byte, leaving contractOwner's low 20 bytes untouched, + // to prove this is a targeted, single-field read -- not an artifact of also breaking + // ownership or anything else sharing the slot. + bytes32 clearedValue = currentValue & bytes32(~(uint256(0xff) << (20 * 8))); + vm.store(diamondAddr, slot, clearedValue); + + assertFalse(IRegistryStatus(diamondAddr).isInitialized()); + assertEq(OwnershipFacet(diamondAddr).owner(), admin, "contractOwner must be untouched"); + } + + /// @dev Test to ensure IDiamondLoupe's own interfaceId is the well-known EIP-2535 value, + /// unaffected by IRegistryStatus (isInitialized) being routed through the same facet. + function testDiamondLoupeInterfaceIdIsStandard() public pure { + assertEq(type(IDiamondLoupe).interfaceId, bytes4(0x48e2b093)); + } +} + +interface INonExistent { + function nonExistent() external; +} + +contract MockRegistryFacet { + function erc20Supra() external pure returns (address) { + return address(0x999); + } + + function counter() external pure returns (uint256) { + return 1; + } +} + +/// @dev Minimal stand-in for a future upgrade initializer, mirroring DiamondInit's own +/// Initializable pattern but tagged `reinitializer(2)` instead of `initializer` -- see +/// DiamondInit.sol's note on Initializable's version being permanently pinned to 1 by the +/// genesis init() call. +contract MockDiamondInitV2 is Initializable { + event ReinitializedV2(); + + function initV2() external reinitializer(2) { + emit ReinitializedV2(); + } +} diff --git a/solidity/supra_contracts/test/ERC20Supra.t.sol b/solidity/supra_contracts/test/ERC20Supra.t.sol new file mode 100644 index 0000000000..60d4874d42 --- /dev/null +++ b/solidity/supra_contracts/test/ERC20Supra.t.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {IERC20Supra} from "../src/interfaces/IERC20Supra.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; + +contract ERC20SupraTest is Test { + ERC20Supra token; + + address owner = address(0x123); + address alice = address(0x456); + address bob = address(0x789); + address bridge = address(0xabc); + address erc20SupraHandlerAddr; + address newAuthorized = address(0xdef); + + function setUp() public { + vm.deal(alice, 100 ether); + vm.deal(bob, 50 ether); + vm.deal(owner, 10 ether); + + erc20SupraHandlerAddr = vm.computeCreateAddress(owner, 3); + address[] memory authorizedAddresses = new address[](2); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = erc20SupraHandlerAddr; + + vm.startPrank(owner); + ERC20Supra impl = new ERC20Supra(); + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (owner, authorizedAddresses)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + token = ERC20Supra(address(proxy)); + vm.stopPrank(); + } + + /// @dev Test to ensure all state variables are initialized correctly. + function testDeployment() public view { + assertEq(token.owner(), owner); + assertEq(token.name(), "ERC20Supra"); + assertEq(token.symbol(), "SUPRA"); + assertEq(token.decimals(), 18); + + assertTrue(token.authorizedAddresses(bridge)); + assertTrue(token.authorizedAddresses(erc20SupraHandlerAddr)); + } + + /// @dev Test to ensure initialization reverts with invalid owner address. + function testInitializeRevertsWithInvalidOwner() public { + address[] memory authorizedAddresses = new address[](2); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = erc20SupraHandlerAddr; + + vm.startPrank(owner); + ERC20Supra impl = new ERC20Supra(); + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (address(0), authorizedAddresses)); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + new ERC1967Proxy(address(impl), initData); + vm.stopPrank(); + } + + /// @dev Test to ensure initialization reverts with invalid address in array. + function testInitializeRevertsWithInvalidAddress() public { + address[] memory authorizedAddresses = new address[](2); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = address(0); // Invalid address + + vm.startPrank(owner); + ERC20Supra impl = new ERC20Supra(); + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (owner, authorizedAddresses)); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + new ERC1967Proxy(address(impl), initData); + vm.stopPrank(); + } + + + /// @dev Test to ensure initialization ignores duplicate addresses and succeeds. + function testInitializeIgnoresDuplicateAddress() public { + address[] memory authorizedAddresses = new address[](3); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = erc20SupraHandlerAddr; + authorizedAddresses[2] = bridge; // Duplicate address + + vm.startPrank(owner); + ERC20Supra impl = new ERC20Supra(); + bytes memory initData = abi.encodeCall(ERC20Supra.initialize, (owner, authorizedAddresses)); + + vm.expectEmit(true, false, false, false); + emit IERC20Supra.InitializedAuthorizedAddresses(authorizedAddresses); + + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + ERC20Supra erc20Supra = ERC20Supra(address(proxy)); + + vm.stopPrank(); + + assertTrue(erc20Supra.authorizedAddresses(bridge)); + assertTrue(erc20Supra.authorizedAddresses(erc20SupraHandlerAddr)); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'mint' ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'mint' works correctly when called by authorized address. + function testMintByAuthorizedAddress() public { + vm.prank(bridge); + token.mint(alice, 100); + + assertEq(token.balanceOf(alice), 100); + } + + /// @dev Test to ensure 'mint' reverts if called by an unauthorized caller. + function testMintRevertsIfUnauthorizedCaller() public { + vm.expectRevert(IERC20Supra.UnauthorizedCaller.selector); + + vm.prank(alice); + token.mint(alice, 100); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'burnFrom' ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'burnFrom' works correctly when called by authorized address. + function testBurnFromByAuthorizedAddress() public { + vm.prank(bridge); + token.mint(alice, 100); + assertEq(token.balanceOf(alice), 100); + + vm.prank(bridge); + token.burnFrom(alice, 50); + + assertEq(token.balanceOf(alice), 50); + } + + /// @dev Test to ensure 'burnFrom' reverts if called by an unauthorized caller. + function testBurnFromRevertsIfUnauthorizedCaller() public { + vm.expectRevert(IERC20Supra.UnauthorizedCaller.selector); + + vm.prank(alice); + token.burnFrom(alice, 10); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'burn' ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'burn' works correctly when called by authorized address. + function testBurn() public { + vm.prank(bridge); + token.mint(bridge, 100); + assertEq(token.balanceOf(bridge), 100); + + vm.prank(bridge); + token.burn(50); + + assertEq(token.balanceOf(bridge), 50); + } + + /// @dev Test to ensure 'burn' reverts if called by an unauthorized caller. + function testBurnRevertsIfUnauthorizedCaller() public { + vm.expectRevert(IERC20Supra.UnauthorizedCaller.selector); + + vm.prank(alice); + token.burn(10); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'addAuthorizedAddress' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure adding authorized address works. + function testAddAuthorizedAddress() public { + vm.prank(owner); + token.addAuthorizedAddress(newAuthorized); + + assertTrue(token.authorizedAddresses(newAuthorized)); + } + + /// @dev Test to ensure 'AuthorizedAddressAdded' event is emitted correctly. + function testAddAuthorizedAddressEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IERC20Supra.AuthorizedAddressAdded(newAuthorized, owner); + + vm.prank(owner); + token.addAuthorizedAddress(newAuthorized); + } + + /// @dev Test to ensure adding authorized address reverts if not owner. + function testAddAuthorizedAddressRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + token.addAuthorizedAddress(newAuthorized); + } + + /// @dev Test to ensure adding invalid address reverts. + function testAddAuthorizedAddressRevertsIfInvalidAddress() public { + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + + vm.prank(owner); + token.addAuthorizedAddress(address(0)); + } + + /// @dev Test to ensure adding already authorized address reverts. + function testAddAuthorizedAddressRevertsIfAlreadyAuthorized() public { + vm.expectRevert(IERC20Supra.AddressAlreadyAuthorized.selector); + + vm.prank(owner); + token.addAuthorizedAddress(bridge); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'removeAuthorizedAddress' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure removing authorized address works. + function testRemoveAuthorizedAddress() public { + vm.prank(owner); + + token.removeAuthorizedAddress(bridge); + assertFalse(token.authorizedAddresses(bridge)); + } + + /// @dev Test to ensure 'AuthorizedAddressRemoved' event is emitted correctly. + function testRemoveAuthorizedAddressEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IERC20Supra.AuthorizedAddressRemoved(bridge, owner); + + vm.prank(owner); + token.removeAuthorizedAddress(bridge); + } + + /// @dev Test to ensure removing authorized address reverts if not owner. + function testRemoveAuthorizedAddressRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + token.removeAuthorizedAddress(bridge); + } + + /// @dev Test to ensure removing non-authorized address reverts. + function testRemoveAuthorizedAddressRevertsIfNotAuthorized() public { + vm.prank(owner); + + vm.expectRevert(IERC20Supra.AddressNotAuthorized.selector); + token.removeAuthorizedAddress(address(0x123)); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'upgradeToAndCall' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'upgradeToAndCall' upgrades the proxy to a new implementation. + function testUpgradeToAndCall() public { + vm.prank(bridge); + token.mint(alice, 50); + assertEq(token.balanceOf(alice), 50); + + vm.prank(owner); + ERC20Supra newImpl = new ERC20Supra(); + + vm.prank(owner); + token.upgradeToAndCall(address(newImpl), ""); + + assertEq(address(uint160(uint256(vm.load(address(token), ERC1967Utils.IMPLEMENTATION_SLOT)))), address(newImpl)); + + vm.prank(bridge); + token.mint(alice, 50); + assertEq(token.balanceOf(alice), 100); + } + + /// @dev Test to ensure 'upgradeToAndCall' reverts if caller is not the owner. + function testUpgradeToAndCallRevertsIfNotOwner() public { + vm.prank(owner); + ERC20Supra newImpl = new ERC20Supra(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + vm.prank(alice); + token.upgradeToAndCall(address(newImpl), ""); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/ERC20SupraHandler.t.sol b/solidity/supra_contracts/test/ERC20SupraHandler.t.sol new file mode 100644 index 0000000000..5047ae4957 --- /dev/null +++ b/solidity/supra_contracts/test/ERC20SupraHandler.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; +import {IERC20SupraHandler} from "../src/interfaces/IERC20SupraHandler.sol"; + +contract ERC20SupraHandlerTest is Test { + ERC20Supra token; + ERC20SupraHandler erc20SupraHandler; + + address owner = address(0x123); + address alice = address(0x456); + address bob = address(0x789); + address bridge = address(0xabc); + + function setUp() public { + vm.deal(alice, 100 ether); + vm.deal(bob, 50 ether); + vm.deal(owner, 10 ether); + + address erc20SupraHandlerAddr = vm.computeCreateAddress(owner, 3); + address[] memory authorizedAddresses = new address[](2); + authorizedAddresses[0] = bridge; + authorizedAddresses[1] = erc20SupraHandlerAddr; + + vm.startPrank(owner); + ERC20Supra erc20SupraImpl = new ERC20Supra(); + bytes memory erc20SupraInitData = abi.encodeCall(ERC20Supra.initialize, (owner, authorizedAddresses)); + ERC1967Proxy erc20SupraProxy = new ERC1967Proxy(address(erc20SupraImpl), erc20SupraInitData); + token = ERC20Supra(address(erc20SupraProxy)); + + ERC20SupraHandler handlerImpl = new ERC20SupraHandler(); + bytes memory handlerInitData = abi.encodeCall(ERC20SupraHandler.initialize, (owner, address(token))); + ERC1967Proxy handlerProxy = new ERC1967Proxy(address(handlerImpl), handlerInitData); + erc20SupraHandler = ERC20SupraHandler(payable(address(handlerProxy))); + vm.stopPrank(); + } + + /// @dev Test to ensure all state variables are initialized correctly. + function testDeployment() public view { + assertEq(erc20SupraHandler.owner(), owner); + assertEq(erc20SupraHandler.erc20Supra(), address(token)); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'deposit' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'deposit' deposits native tokens and mints ERC20Supra tokens 1:1. + function testDeposit() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 5 ether}(); + + assertEq(token.balanceOf(alice), 5 ether); + assertEq(address(erc20SupraHandler).balance, 5 ether); + assertEq(address(erc20SupraHandler).balance, token.totalSupply()); + assertEq(alice.balance, 95 ether); + } + + /// @dev Test to ensure 'deposit' emits event. + function testDepositEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IERC20SupraHandler.Deposit(alice, 5 ether); + + vm.prank(alice); + erc20SupraHandler.deposit{value: 5 ether}(); + } + + /// @dev Test to ensure 'deposit' reverts if amount sent is zero. + function testDepositRevertsIfAmountZero() public { + vm.expectRevert(IERC20SupraHandler.InvalidAmount.selector); + + vm.prank(alice); + erc20SupraHandler.deposit{value: 0}(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'receive' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure sending native tokens direcly mints ERC20Supra tokens 1:1. + function testReceiveMintsERC20Supra() public { + vm.prank(alice); + (bool success, ) = address(erc20SupraHandler).call{value: 3 ether}(""); + require(success); + + assertEq(token.balanceOf(alice), 3 ether); + assertEq(address(erc20SupraHandler).balance, 3 ether); + assertEq(alice.balance, 97 ether); + } + + /// @dev Test to ensure 'receive' emits event. + function testReceiveEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IERC20SupraHandler.Deposit(alice, 3 ether); + + vm.prank(alice); + (bool success, ) = address(erc20SupraHandler).call{value: 3 ether}(""); + require(success); + } + + /// @dev Test to ensure 'receive' reverts if amount sent is zero. + function testReceiveRevertsIfAmountZero() public { + vm.prank(alice); + (bool success, bytes memory data) = address(erc20SupraHandler).call{value: 0}(""); + + assertFalse(success); + assertEq(bytes4(data), IERC20SupraHandler.InvalidAmount.selector); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'withdraw' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'withdraw' withdraws native tokens and burns ERC20Supra 1:1. + function testWithdraw() public { + // Alice deposits 5 SUPRA → gets 5 * 10 ** 18 ERC20Supra tokens + testDeposit(); + + // Alice withdraws 3 SUPRA → burns 3 * 10 ** 18 ERC20Supra tokens + vm.prank(alice); + erc20SupraHandler.withdraw(3 ether); + + assertEq(token.balanceOf(alice), 2 ether); + assertEq(address(alice).balance, 98 ether); + assertEq(address(erc20SupraHandler).balance, 2 ether); + assertEq(address(erc20SupraHandler).balance, token.totalSupply()); + } + + /// @dev Test to ensure 'withdraw' emits event. + function testWithdrawEmitsEvent() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 5 ether}(); + + vm.expectEmit(true, true, false, false); + emit IERC20SupraHandler.Withdrawal(alice, 2 ether); + + vm.prank(alice); + erc20SupraHandler.withdraw(2 ether); + } + + /// @dev Test to ensure 'withdraw' reverts if balance is less than requested amount. + function testWithdrawRevertsIfInsufficientBalance() public { + vm.expectRevert(IERC20SupraHandler.InsufficientBalance.selector); + + vm.prank(alice); + erc20SupraHandler.withdraw(1 ether); + } + + /// @dev Test to ensure 'withdraw' reverts if contract balance is less than requested amount. + function testWithdrawRevertsIfInsufficientContractBalance() public { + vm.prank(bridge); + token.mint(alice, 1 ether); + + vm.expectRevert(IERC20SupraHandler.InsufficientContractBalance.selector); + + vm.prank(alice); + erc20SupraHandler.withdraw(1 ether); + } + + /// @dev Test to ensure 'withdraw' reverts if requested amount is zero. + function testWithdrawRevertsIfAmountZero() public { + vm.expectRevert(IERC20SupraHandler.InvalidAmount.selector); + + vm.prank(alice); + erc20SupraHandler.withdraw(0); + } + + /// @notice Test to ensure that `withdraw` reverts if the native token transfer fails. + /// @dev This test uses a contract that always reverts on receiving native token to simulate a failing low-level call. + function testWithdrawRevertsIfNativeTransferFails() public { + // Mint tokens + vm.prank(alice); + erc20SupraHandler.deposit{value: 1 ether}(); + + RejectReceive rejector = new RejectReceive(); + + // Transfer tokens to the rejecting contract + vm.prank(alice); + bool success = token.transfer(address(rejector), 1 ether); + assertTrue(success); + + // Attempt withdrawal → should revert + vm.expectRevert(IERC20SupraHandler.TransferFailed.selector); + + vm.prank(address(rejector)); + erc20SupraHandler.withdraw(1 ether); + + assertEq(token.balanceOf(address(rejector)), 1 ether); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Additional test cases for ERC20Supra :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure transfer of tokens between users works correctly. + function testTransferBetweenUsers() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 5 ether}(); + + assertEq(token.balanceOf(alice) , 5 ether); + + vm.prank(alice); + bool success = token.transfer(bob, 2 ether); + assertTrue(success); + + assertEq(token.balanceOf(alice), 3 ether); + assertEq(token.balanceOf(bob), 2 ether); + } + + /// @dev Test to ensure 'transferFrom' works correctly after allowance is granted. + function testTransferFromAllowance() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 5 ether}(); + + vm.prank(alice); + token.approve(bob, 3 ether); + + vm.prank(bob); + bool success = token.transferFrom(alice, bob, 2 ether); + assertTrue(success); + + assertEq(token.balanceOf(alice), 3 ether); + assertEq(token.balanceOf(bob), 2 ether); + assertEq(token.allowance(alice, bob), 1 ether); + } + + /// @dev Test to ensure 'totalSupply' is equal to the balance of ERC20Supra contract. + function testTotalSupplyEqualsContractBalance() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 3 ether}(); + vm.prank(bob); + erc20SupraHandler.deposit{value: 2 ether}(); + + vm.prank(alice); + erc20SupraHandler.withdraw(1 ether); + vm.prank(bob); + erc20SupraHandler.withdraw(2 ether); + + assertEq(address(erc20SupraHandler).balance, token.totalSupply()); + assertEq(token.totalSupply(), 2 ether); + assertEq(token.balanceOf(alice), 2 ether); + assertEq(token.balanceOf(bob), 0); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'upgradeToAndCall' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'upgradeToAndCall' upgrades the proxy to a new implementation. + function testUpgradeToAndCall() public { + vm.prank(alice); + erc20SupraHandler.deposit{value: 1 ether}(); + assertEq(token.balanceOf(alice), 1 ether); + + vm.startPrank(owner); + ERC20SupraHandler newImpl = new ERC20SupraHandler(); + erc20SupraHandler.upgradeToAndCall(address(newImpl), ""); + vm.stopPrank(); + + assertEq(address(uint160(uint256(vm.load(address(erc20SupraHandler), ERC1967Utils.IMPLEMENTATION_SLOT)))), address(newImpl)); + + vm.prank(alice); + erc20SupraHandler.deposit{value: 1 ether}(); + assertEq(token.balanceOf(alice), 2 ether); + } + + /// @dev Test to ensure 'upgradeToAndCall' reverts if caller is not the owner. + function testUpgradeToAndCallRevertsIfNotOwner() public { + vm.prank(owner); + ERC20SupraHandler newImpl = new ERC20SupraHandler(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + vm.prank(alice); + erc20SupraHandler.upgradeToAndCall(address(newImpl), ""); + } +} + +/// @notice Helper contract that rejects all incoming native token transfers. +contract RejectReceive { + fallback() external payable { revert(); } + receive() external payable { revert(); } +} diff --git a/solidity/supra_contracts/test/MonitorCycleEndGas.t.sol b/solidity/supra_contracts/test/MonitorCycleEndGas.t.sol new file mode 100644 index 0000000000..d2c46ffeec --- /dev/null +++ b/solidity/supra_contracts/test/MonitorCycleEndGas.t.sol @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {console} from "forge-std/console.sol"; +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {LibAppStorage, RegistryState} from "../src/libraries/LibAppStorage.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; + +/// @notice Gas-scaling tests for monitorCycleEnd / onCycleEndInternal. +/// +/// The Supra native layer calls `monitorCycleEnd` from `BlockMeta::blockPrologue` +/// with a hard gas budget of 16_777_216. The function's cost scales linearly with +/// the number of registered tasks, regardless of removal history, because +/// `onCycleEndInternal` must: +/// 1. Filter+compact RegistryState.orderedTaskIds into the alive-only ascending +/// list (LibCore.buildAliveOrderedTaskIds — O(n), one SLOAD per task) +/// 2. Write the result into the transition state's `expectedTasksToBeProcessed` (SSTORE per task) +/// +/// Step 2 dominates. `expectedTasksToBeProcessed` used to be an EnumerableSet.UintSet, +/// whose add() writes two 20,000-gas SSTOREs per task (one for the value array element, +/// one for the O(1)-lookup index mapping entry) — ~40,000-45,000 gas/task. That mapping +/// was never actually queried (the field is only ever read back sequentially via +/// length/at), so the field was changed to a plain `uint256[]`: a single push() per task +/// now costs one SSTORE instead of two, roughly halving the per-task cost to +/// ~20,000-22,000 gas (see LibCore.updateExpectedTasks and its NatSpec). +/// +/// This test file answers two questions empirically: +/// A. What gas does monitorCycleEnd consume for a given task count? +/// B. At what task count does the cumulative gas exceed 16_777_216? +/// +/// Every test logs its gas figures so the output of `forge test -vv` can be used +/// as a reference when sizing the task registry capacity. +contract MonitorCycleEndGasTest is BaseDiamondTest { + + /// @dev Hard gas budget imposed by Supra's blockPrologue on this call path. + uint256 constant BLOCK_PROLOGUE_GAS_LIMIT = 16_777_216; + + /// @dev Task capacity for the shared diamond used by the N050/N100/N150 point tests + /// and by the BoundaryScan, which searches [1, LARGE_CAPACITY]. + /// N200-N400 tests each deploy a diamond with a capacity matching their own N, + /// so they are unaffected by this constant. + /// uint16 matches the type of InitParams.taskCapacity. + uint16 constant LARGE_CAPACITY = 1000; + + // ──────────────────────────────────────────────────────────────────────── + // Helpers + // ──────────────────────────────────────────────────────────────────────── + + /// @dev Deploy a fresh diamond whose task capacity is set to `_capacity`. + /// Using a dedicated diamond per test keeps each measurement independent — + /// storage that was touched by a prior test would be "warm" and distort + /// the SLOAD/SSTORE gas figures. + function _deployWithCapacity(uint16 _capacity) internal returns (address diamond) { + InitParams memory p = LibDiamondUtils.defaultInitParams(); + p.taskCapacity = _capacity; + // Keep cycle short so vm.warp does not need a large jump. + p.cycleDurationSecs = 1200; + // Raise the registry gas cap so it never blocks registration for large N. + // LARGE_CAPACITY * 100_000 (maxGasAmount per task) needs to fit in this cap. + // The production cap is a separate concern; here we only want to measure + // monitorCycleEnd gas without being gated by the registration cap. + p.registryMaxGasCap = uint128(uint256(_capacity) * 100_000 + 1_000_000); + + vm.startPrank(admin); + Deployment memory d = LibDiamondUtils.deploy(admin, address(erc20Supra), p); + diamond = d.diamond; + vm.stopPrank(); + } + + /// @dev Register `_n` USTs on `_diamond`. + /// + /// Token flow: + /// - Each registration deducts `flatRegistrationFeeWei` (1 ether) from alice's + /// ERC20 balance. We deposit `_n * 2 ether` ETH upfront so alice has enough + /// ERC20 for all registrations without running dry. + /// - The automation fee cap (60.1 ether) is per-task but is only *charged* during + /// processTasks, not during monitorCycleEnd. It does not affect this gas test. + function _registerNTasks(address _diamond, uint256 _n) internal { + // Each registration deducts two amounts from alice's ERC20 balance: + // - flatRegistrationFeeWei = 1 ether (charged and collected) + // - automationFeeCapForCycle = 60.1 ether (locked as deposit) + // Total per task: 61.1 ether. We deposit _n * 62 ether to include a + // small per-task buffer on top of the exact 61.1 ether requirement. + uint256 depositAmount = _n * 62 ether; + vm.deal(alice, depositAmount + 100 ether); + + bytes[] memory auxData; + bytes memory payload = createPayload( + 0, + address(erc20SupraHandler), + abi.encodeCall(erc20SupraHandler.withdraw, 100) + ); + bytes memory predicate = createPredicate(_diamond); + + vm.startPrank(alice); + // Single bulk deposit — mints depositAmount of ERC20 to alice. + erc20SupraHandler.deposit{value: depositAmount}(); + erc20Supra.approve(_diamond, type(uint256).max); + + // Expiry is set to 1 day from the current block.timestamp. + // Using 3600 (1 hour) was too close to the cycle end (1200 s) when + // block.timestamp accumulates across binary-scan iterations. + // 86400 s is well within the 7-day taskDurationCapSecs cap and is always + // many times larger than cycleDurationSecs (1200 s), so the + // TaskExpiresBeforeNextCycle check (expiryTime > cycleEndTime) always passes + // regardless of how many vm.warp calls have occurred before registration. + uint64 expiry = uint64(block.timestamp + 86400); + + for (uint256 i = 0; i < _n; i++) { + IRegistryFacet(_diamond).register( + payload, + predicate, + expiry, + uint128(100_000), // maxGasAmount + uint128(4 gwei), // gasPriceCap + uint128(60.1 ether), // automationFeeCapForCycle + 2, // priority + auxData + ); + } + vm.stopPrank(); + } + + /// @dev Advance time past the cycle boundary, call monitorCycleEnd as the + /// VM signer, and return the gas consumed. + /// + /// Gas metering is done with gasleft() brackets rather than Forge's + /// `vm.pauseGasMetering` so that storage-access costs (SLOAD/SSTORE) + /// are captured faithfully — pauseGasMetering would zero those out. + /// + /// The measured value is slightly higher than the bare function cost + /// because the CALL opcode overhead and return data copying are included. + /// This conservative over-count is appropriate: the production caller + /// (blockPrologue) incurs the same overhead. + function _measureMonitorCycleEnd(address _diamond) internal returns (uint256 gasUsed) { + // Warp to exactly the cycle boundary so the end condition triggers. + (, uint64 startTime, uint64 durationSecs,) = ICoreFacet(_diamond).getCycleInfo(); + vm.warp(startTime + durationSecs); + + // monitorCycleEnd checks tx.origin, not msg.sender alone. + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + + uint256 before = gasleft(); + ICoreFacet(_diamond).monitorCycleEnd(); + gasUsed = before - gasleft(); + } + + /// @dev Overwrites `RegistryState.orderedTaskIds` (the append-only array + /// `buildAliveOrderedTaskIds`, LibCore.sol, actually reads at cycle end) with + /// `n-1, n-2, ..., 0` (fully descending) instead of the `0..n-1` (ascending) + /// order `_registerNTasks` leaves it in. + /// + /// This is a regression guard: `orderedTaskIds` is append-only in production + /// (LibRegistry.createAndStoreTask) and task IDs are assigned strictly + /// monotonically, so it can never actually become descending on its own. Forcing + /// it here proves `buildAliveOrderedTaskIds`'s cost has no dependency on element + /// order (it's a filter, not a sort) — this test's result should match + /// testMonitorCycleEndGas_BoundaryScan's regardless of input order. + /// + /// Slot derivation: the base is not hand-computed at all -- `LibAppStorage.registryState()` + /// already correctly resolves `AppStorage.registry[REGISTRY_STATE]`'s storage slot (it's + /// the exact same accessor every facet uses), so reading its `.slot` via assembly gives + /// the real, current `RegistryState` base regardless of `AppStorage`'s own field order or + /// base position (`LibAppStorage.APP_STORAGE_POSITION`) -- neither of those can ever go + /// stale here, because nothing about them is repeated as a literal below. + /// + /// The one number that IS a literal is `orderedTaskIds`'s offset *within* `RegistryState` + /// (relative slot 11, plain `uint256[]`, confirmed via `forge inspect storage-layout --json`, not hand-computed) -- this is + /// the one that moves if `RegistryState`'s own field order changes, and there's no per-field + /// accessor to derive it from, so it must be re-verified against `forge inspect ... + /// storage-layout --json` whenever `RegistryState`'s field order changes. + /// `testMonitorCycleEndGas_BoundaryScan_ReverseSorted` additionally asserts + /// the write actually landed (see its call to `_assertStrictlyDescending`), + /// so a future layout shift fails the test loudly instead of leaving it + /// silently vacuous. + function _setOrderedTaskIdsDescending(address _diamond, uint256 _n) internal { + RegistryState storage rs = LibAppStorage.registryState(); + uint256 registryStateBase; + assembly { + registryStateBase := rs.slot + } + uint256 lengthSlot = registryStateBase + 11; + uint256 dataBase = uint256(keccak256(abi.encode(lengthSlot))); + + vm.store(_diamond, bytes32(lengthSlot), bytes32(_n)); + for (uint256 i = 0; i < _n; i++) { + // Registered task IDs are 0..n-1 (ascending); write them back descending + // (n-1, n-2, ..., 0) so the array holds exactly the same value set, + // just in the fully-reversed order. + vm.store(_diamond, bytes32(dataBase + i), bytes32((_n - 1) - i)); + } + } + + /// @dev Asserts `_arr` is strictly descending. Used to confirm + /// `_setOrderedTaskIdsDescending`'s vm.store calls actually landed on + /// `orderedTaskIds`: `buildAliveOrderedTaskIds` (LibCore.sol) is a filter, + /// not a sort, so it preserves input order - `expectedTasksToBeProcessed` + /// can only come back descending if the storage write reached the array + /// it's derived from. If a future AppStorage layout change moves the + /// target slot again, this fails the test instead of letting it silently + /// degrade into a duplicate of the ascending scan. + function _assertStrictlyDescending(uint64[] memory _arr) internal pure { + for (uint256 i = 1; i < _arr.length; i++) { + assertLt(_arr[i], _arr[i - 1], "expectedTasksToBeProcessed not descending - orderedTaskIds overwrite did not land (storage-slot drift?)"); + } + } + + // ──────────────────────────────────────────────────────────────────────── + // Individual measurements + // + // Each test is a self-contained: fresh diamond, register N tasks, measure. + // Running them under `forge test -vv` prints the gas figures. + // ──────────────────────────────────────────────────────────────────────── + + function testMonitorCycleEndGas_N050() public { + address d = _deployWithCapacity(LARGE_CAPACITY); + _registerNTasks(d, 50); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=050 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=50 must be within gas budget"); + } + + function testMonitorCycleEndGas_N100() public { + address d = _deployWithCapacity(LARGE_CAPACITY); + _registerNTasks(d, 100); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=100 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=100 must be within gas budget"); + } + + function testMonitorCycleEndGas_N150() public { + address d = _deployWithCapacity(LARGE_CAPACITY); + _registerNTasks(d, 150); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=150 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=150 must be within gas budget"); + } + + function testMonitorCycleEndGas_N200() public { + // Deploy with capacity exactly matching N so the task-count limit never + // triggers, regardless of the LARGE_CAPACITY constant used by smaller tests. + address d = _deployWithCapacity(200); + _registerNTasks(d, 200); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=200 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=200 must be within gas budget"); + } + + function testMonitorCycleEndGas_N250() public { + address d = _deployWithCapacity(250); + _registerNTasks(d, 250); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=250 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=250 must be within gas budget"); + } + + function testMonitorCycleEndGas_N300() public { + address d = _deployWithCapacity(300); + _registerNTasks(d, 300); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=300 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=300 must be within gas budget"); + } + + function testMonitorCycleEndGas_N350() public { + address d = _deployWithCapacity(350); + _registerNTasks(d, 350); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=350 |", gas); + assertLt(gas, BLOCK_PROLOGUE_GAS_LIMIT, "N=350 must be within gas budget"); + } + + function observeMonitorCycleEndGas_N720() public { + address d = _deployWithCapacity(720); + _registerNTasks(d, 720); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=720 |", gas); + // No hard assertion: the boundary scan below identifies the exact safe limit + // (see testMonitorCycleEndGas_BoundaryScan), which sits well above the production + // capacity of 200 (taskCapacity + sysTaskCapacity, LibDiamondUtils.sol). + if (gas >= BLOCK_PROLOGUE_GAS_LIMIT) { + console.log(" -> N=720 EXCEEDS budget (", BLOCK_PROLOGUE_GAS_LIMIT, ")"); + } else { + console.log(" -> N=720 within budget"); + } + } + + function testMonitorCycleEndGas_N800() public { + address d = _deployWithCapacity(800); + _registerNTasks(d, 800); + uint256 gas = _measureMonitorCycleEnd(d); + console.log("monitorCycleEnd gas | N=800 |", gas); + // N=800 is the registry capacity ceiling; log whether it fits the budget + // without a hard assertion so CI does not break if it is over budget — + // the boundary scan test below identifies the exact safe limit. + if (gas >= BLOCK_PROLOGUE_GAS_LIMIT) { + console.log(" -> N=800 EXCEEDS budget (", BLOCK_PROLOGUE_GAS_LIMIT, ")"); + } else { + console.log(" -> N=800 within budget"); + } + } + + // ──────────────────────────────────────────────────────────────────────── + // Boundary scan + // + // Binary-searches for the largest N such that monitorCycleEnd stays within + // BLOCK_PROLOGUE_GAS_LIMIT, using the full [1, LARGE_CAPACITY] range. + // + // This is the canonical test for answering "what is the safe task limit?". + // Run it once and read the logged result; rerun after any algorithm change. + // ──────────────────────────────────────────────────────────────────────── + function testMonitorCycleEndGas_BoundaryScan() public { + // Deploy the diamond ONCE before the search loop and snapshot its clean state. + // + // Rationale for deploy-once: previous versions deployed inside the loop, paying + // ~12.7 M gas per iteration for facet deployments. With LARGE_CAPACITY = 198 and + // ~8 binary-search iterations, that added ~100 M gas to the total — pushing the + // test past the 1 billion gas ceiling even after raising block_gas_limit. + // + // The snapshot is taken AFTER deployment so block.timestamp already reflects a + // real cycle-start reference ("proper cycle start time" precondition). Every + // binary-search iteration reverts to this snapshot, which: + // - removes all task registrations from the previous probe + // - resets block.timestamp to the clean post-deployment value, preventing + // timestamp drift from accumulated vm.warp calls (which would otherwise cause + // TaskExpiresBeforeNextCycle once enough cycles have elapsed) + // + // Local Solidity variables (lo, hi, safeLimitN, mid, gas) are stack-allocated + // and are NOT part of EVM state, so they survive vm.revertToState unchanged. + // vm.revertToState (not revertToStateAndDelete) keeps the snapshot alive for + // reuse in subsequent iterations. + address d = _deployWithCapacity(LARGE_CAPACITY); + uint256 cleanSnap = vm.snapshotState(); + + uint256 lo = 1; + uint256 hi = LARGE_CAPACITY; + uint256 safeLimitN = 0; + + while (lo <= hi) { + uint256 mid = (lo + hi) / 2; + + _registerNTasks(d, mid); + uint256 gas = _measureMonitorCycleEnd(d); + + vm.revertToState(cleanSnap); + + if (gas < BLOCK_PROLOGUE_GAS_LIMIT) { + safeLimitN = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + console.log("=== monitorCycleEnd gas boundary (buildAliveOrderedTaskIds, ascending taskIdList) ==="); + console.log("Safe task limit (max N within 16_777_216 gas):", safeLimitN); + console.log("First N that exceeds budget :", safeLimitN + 1); + + assertGt(safeLimitN, 0, "no safe N found - even N=1 exceeds budget"); + } + + // ──────────────────────────────────────────────────────────────────────── + // Boundary scan — reverse-sorted orderedTaskIds (regression guard) + // + // Identical binary search to testMonitorCycleEndGas_BoundaryScan, except + // that after each probe's registrations, orderedTaskIds' underlying array is + // overwritten (via _setOrderedTaskIdsDescending) to be fully descending + // instead of the ascending order registration naturally produces. Since + // buildAliveOrderedTaskIds (LibCore.sol) is a linear filter, not a sort, its + // cost has no dependency on element order — this test exists to prove that + // property empirically and catch any future regression back toward an + // order-sensitive algorithm. Expect this to report the same safe limit as + // testMonitorCycleEndGas_BoundaryScan. + // ──────────────────────────────────────────────────────────────────────── + function testMonitorCycleEndGas_BoundaryScan_ReverseSorted() public { + address d = _deployWithCapacity(LARGE_CAPACITY); + uint256 cleanSnap = vm.snapshotState(); + + uint256 lo = 1; + uint256 hi = LARGE_CAPACITY; + uint256 safeLimitN = 0; + + while (lo <= hi) { + uint256 mid = (lo + hi) / 2; + + _registerNTasks(d, mid); + _setOrderedTaskIdsDescending(d, mid); + uint256 gas = _measureMonitorCycleEnd(d); + + // Confirms the vm.store calls above actually landed on orderedTaskIds - + // see _assertStrictlyDescending's NatSpec for why this is load-bearing, + // not decorative. + _assertStrictlyDescending(ICoreFacet(d).getCycleStateDetails().expectedTasksToBeProcessed); + + vm.revertToState(cleanSnap); + + if (gas < BLOCK_PROLOGUE_GAS_LIMIT) { + safeLimitN = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + console.log("=== monitorCycleEnd gas boundary (buildAliveOrderedTaskIds, FULLY REVERSE-SORTED orderedTaskIds) ==="); + console.log("Safe task limit (max N within 16_777_216 gas):", safeLimitN); + console.log("First N that exceeds budget :", safeLimitN + 1); + + assertGt(safeLimitN, 0, "no safe N found - even N=1 exceeds budget"); + } +} diff --git a/solidity/supra_contracts/test/MultiSignatureWallet.t.sol b/solidity/supra_contracts/test/MultiSignatureWallet.t.sol new file mode 100644 index 0000000000..35c164d393 --- /dev/null +++ b/solidity/supra_contracts/test/MultiSignatureWallet.t.sol @@ -0,0 +1,1417 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "./Counter.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {MultiSignatureWallet} from "../src/MultiSignatureWallet.sol"; +import {IMultiSignatureWallet} from "../src/interfaces/IMultiSignatureWallet.sol"; +import {MultisigBeacon, UpgradeableBeacon} from "../src/MultisigBeacon.sol"; + +contract MultiSignatureWalletTest is Test { + Counter counter; + MultisigBeacon beacon; + address multiSigImplV1; + MultiSignatureWallet multiSig; + + address[] owners; + address[] newOwners; + address alice = address(0xA11CE); + + /// @dev Sets up initial state for testing. + /// @dev Deploys all required contracts. + function setUp() public { + vm.deal(alice, 10 ether); + + address owner1 = address(1001); + address owner2 = address(1002); + address owner3 = address(1003); + address owner4 = address(1004); + address owner5 = address(1005); + owners.push(owner1); + owners.push(owner2); + owners.push(owner3); + owners.push(owner4); + owners.push(owner5); + + vm.startPrank(alice); + // Deploy Beacon contract + multiSigImplV1 = address(new MultiSignatureWallet()); + beacon = new MultisigBeacon(multiSigImplV1, 0xE64Bd5C4810e6C7666C544a05c980C9Fe617283f); // Pre-determined address of multisigProxy + + // Deploy BeaconProxy for MultiSig + bytes memory multiSigInitData = abi.encodeCall(MultiSignatureWallet.initialize, (owners, 4)); + BeaconProxy multisigProxy = new BeaconProxy(address(beacon), multiSigInitData); + multiSig = MultiSignatureWallet(payable(multisigProxy)); + + vm.stopPrank(); + + + vm.startPrank(address(multisigProxy)); + // Deploy Counter proxy contract + Counter counterImpl = new Counter(); + bytes memory counterInitData = abi.encodeCall(Counter.initialize, (address(multiSig))); + ERC1967Proxy counterProxy = new ERC1967Proxy(address(counterImpl), counterInitData); + counter = Counter(address(counterProxy)); + vm.stopPrank(); + } + + /// @dev Test to ensure ownership and implementation address is initialized correctly. + function testOwnerAndImplementation() public view { + assertEq(beacon.owner(), address(multiSig)); + assertEq(counter.owner(), address(multiSig)); + assertEq(beacon.implementation(), multiSigImplV1); + } + + /// @dev Test to ensure contract is initialized correctly. + function testInitialize() public view { + assertEq(multiSig.getOwners(), owners); + assertEq(multiSig.numConfirmationsRequired(), 4); + assertEq(multiSig.txCount(), 0); + } + + /// @dev Test to ensure 'initialize' reverts if array of owners is empty. + function testInitializeRevertsIfOwnersArrayEmpty() public { + address[] memory emptyOwners; + vm.expectRevert(IMultiSignatureWallet.OwnersRequired.selector); + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (emptyOwners, 1)); + new BeaconProxy(address(beacon), initData); + } + + /// @dev Test to ensure 'initialize' reverts if number of confirmations required is zero. + function testInitializeRevertsIfNumConfirmationsZero() public { + vm.expectRevert(IMultiSignatureWallet.InvalidNumberOfConfirmations.selector); + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (owners, 0)); + new BeaconProxy(address(beacon), initData); + } + + /// @dev Test to ensure 'initialize' reverts if number of confirmations required is more than the number of owners. + function testInitializeRevertsIfNumConfirmationsMoreThanOwners() public { + vm.expectRevert(IMultiSignatureWallet.InvalidNumberOfConfirmations.selector); + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (owners, 6)); + new BeaconProxy(address(beacon), initData); + } + + /// @dev Test to ensure 'initialize' reverts if any of the owner is address(0). + function testInitializeRevertsIfOwnerAddressZero() public { + address[] memory invalidOwners = new address[](3); + invalidOwners[0] = address(1001); + invalidOwners[1] = address(0); // Invalid owner + invalidOwners[2] = address(1002); + + vm.expectRevert(IMultiSignatureWallet.InvalidOwner.selector); + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (invalidOwners, 1)); + new BeaconProxy(address(beacon), initData); + } + + /// @dev Test to ensure 'initialize' reverts if a duplicate owner is passed. + function testInitializeRevertsIfDuplicateOwner() public { + address[] memory duplicateOwners = new address[](3); + duplicateOwners[0] = address(1001); + duplicateOwners[1] = address(1001); // Duplicate owner + duplicateOwners[2] = address(1002); + + vm.expectRevert(IMultiSignatureWallet.OwnerNotUnique.selector); + + bytes memory initData = abi.encodeCall(MultiSignatureWallet.initialize, (duplicateOwners, 1)); + new BeaconProxy(address(beacon), initData); + } + + /// @dev Helper function that returns calldata for 'increment' in Counter. + function dataForIncrement() private pure returns (bytes memory) { + return abi.encodeCall(Counter.increment, ()); + } + + /// @dev Helper function to submit a transaction to perform an action in the Counter contract. + function submitTransaction(bytes memory _data) private { + vm.prank(address(1001)); + multiSig.submitTransaction( + address(counter), + 0, + 10000, + _data + ); + } + + /// @dev Test to ensure 'submitTransaction' submits a transaction. + function testSubmitTransactionIncrement() public { + bytes memory data = dataForIncrement(); + submitTransaction(data); + + (address to, uint256 value, uint24 numConfirmations, uint64 timeout, bytes memory storedData) = multiSig.getTransaction(0); + assertEq(to, address(counter)); + assertEq(value, 0); + assertEq(numConfirmations, 1); + assertEq(timeout, block.timestamp + 10000); + assertEq(storedData, data); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'submitTransaction' reverts if caller is not an owner. + function testSubmitTransactionIncrementRevertsIfNotOwner() public { + bytes memory data = dataForIncrement(); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + + vm.prank(alice); // Not an owner + multiSig.submitTransaction( + address(counter), + 0, + 100000, + data + ); + } + + /// @dev Test to ensure 'submitTransaction' reverts if address(0) is passed as recipient. + function testSubmitTransactionIncrementRevertsIfAddressZero() public { + bytes memory data = dataForIncrement(); + + vm.expectRevert(IMultiSignatureWallet.InvalidRecipient.selector); + + vm.prank(address(1001)); + multiSig.submitTransaction( + address(0), + 0, + 100000, + data + ); + } + + /// @dev Helper function to confirm a transaction. + function confirmTransaction(address _owner, uint256 _txnId) private { + vm.prank(_owner); + multiSig.confirmTransaction(_txnId); + } + + /// @dev Helper function to grant sufficient confirmations. + function grantSufficientConfirmations(uint256 _txnId) private { + confirmTransaction(address(1002), _txnId); + confirmTransaction(address(1003), _txnId); + confirmTransaction(address(1004), _txnId); + } + + /// @dev Test to ensure 'confirmTransaction' confirms a transaction. + function testConfirmTransactionIncrement() public { + testSubmitTransactionIncrement(); + + grantSufficientConfirmations(0); + + ( , , uint256 numConfirmations, , ) = multiSig.getTransaction(0); + assertEq(numConfirmations, 4); + } + + /// @dev Test to ensure 'confirmTransaction' reverts if caller is not an owner. + function testConfirmTransactionRevertsIfNotOwner() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + confirmTransaction(alice, 0); // Not an owner + } + + /// @dev Test to ensure 'confirmTransaction' reverts if transaction does not exist. + function testConfirmTransactionRevertsIfTxDoesNotExist() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + confirmTransaction(address(1002), 1); + } + + /// @dev Test to ensure 'confirmTransaction' reverts if the transaction is already executed. + function testConfirmTransactionRevertsIfTxAlreadyExecuted() public { + testSubmitTransactionIncrement(); + + uint256 txId = 0; + grantSufficientConfirmations(txId); + + vm.prank(address(1002)); + multiSig.executeTransaction(txId); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + + confirmTransaction(address(1005), txId); + } + + /// @dev Test to ensure 'confirmTransaction' reverts if transaction is already confirmed. + function testConfirmTransactionRevertsIfTxAlreadyConfirmed() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.TxnAlreadyConfirmed.selector); + confirmTransaction(address(1001), 0); + } + + /// @dev Test to ensure 'confirmTransaction' reverts if the transaction has expired. + function testConfirmTransactionRevertsIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + confirmTransaction(address(1005), 0); + + assertEq(multiSig.txCount(), 1); + } + + /// @dev Helper function to revoke confirmation. + function revokeConfirmation(address _owner, uint256 _txIndex) private { + vm.prank(_owner); + multiSig.revokeConfirmation(_txIndex); + } + + /// @dev Test to ensure 'revokeConfirmation' revokes the confirmation of an owner. + function testRevokeConfirmation() public { + testSubmitTransactionIncrement(); + + uint256 txId = 0; + confirmTransaction(address(1002), txId); + revokeConfirmation(address(1001), txId); + + ( , , uint256 confirmations , , ) = multiSig.getTransaction(txId); + assertEq(confirmations, 1); + assertFalse(multiSig.isConfirmed(txId, address(1001))); + } + + /// @dev Test to ensure 'revokeConfirmation' reverts if caller is not an owner. + function testRevokeConfirmationRevertsIfNotOwner() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + revokeConfirmation(alice, 1); + } + + /// @dev Test to ensure 'revokeConfirmation' reverts if transaction does not exist. + function testRevokeConfirmationRevertsIfTxDoesNotExist() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + revokeConfirmation(address(1001), 1); + } + + /// @dev Test to ensure 'revokeConfirmation' reverts if the transaction is already executed. + function testRevokeConfirmationRevertsIfTxAlreadyExecuted() public { + testSubmitTransactionIncrement(); + + uint256 txId = 0; + grantSufficientConfirmations(txId); + + vm.prank(address(1002)); + multiSig.executeTransaction(txId); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + revokeConfirmation(address(1001), txId); + } + + /// @dev Test to ensure 'revokeConfirmation' reverts if the transaction has expired. + function testRevokeConfirmationRevertsIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + revokeConfirmation(address(1001), 0); + + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'revokeConfirmation' reverts if the transaction was not confirmed. + function testRevokeConfirmationRevertsIfTxNotConfirmed() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.TransactionNotConfirmed.selector); + revokeConfirmation(address(1002), 0); + } + + /// @dev Test to ensure an owner can still revoke a confirmation that has gone stale (owner + /// removed and re-added since confirming), clearing their own now-inert set entry instead of + /// being permanently stuck as an unrevocable member. + function testRevokeConfirmationClearsStaleConfirmation() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); + + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + addOwnerViaMultiSig(address(1002), 2, address(1003), address(1004), address(1005)); + + // owner(1002)'s pre-removal confirmation is stale and already excluded from the count, + // but they must still be able to explicitly revoke and clean it up. + revokeConfirmation(address(1002), 0); + assertFalse(multiSig.isConfirmed(0, address(1002))); + } + + /// @dev Test to ensure 'executeTransaction' executes a transaction. + function testExecuteTransaction() public { + testSubmitTransactionIncrement(); + + uint256 txId = 0; + grantSufficientConfirmations(txId); + + vm.prank(address(1001)); + multiSig.executeTransaction(txId); + + assertEq(multiSig.txCount(), 0); + assertEq(counter.counter(), 1); + } + + /// @dev Test to ensure 'executeTransaction' reverts if caller is not an owner. + function testExecuteTransactionRevertsIfCallerNotOwner() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + + vm.prank(alice); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'executeTransaction' reverts if transaction does not exist. + function testExecuteTransactionRevertsIfTxDoesNotExist() public { + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(1); + } + + /// @dev Test to ensure 'executeTransaction' reverts if transaction is already executed. + function testExecuteTransactionRevertsIfTxAlreadyExecuted() public { + testExecuteTransaction(); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'executeTransaction' reverts if transaction has expired. + function testExecuteTransactionRevertsIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'executeTransaction' reverts if the transaction has insufficient number of confirmations. + function testExecuteTransactionRevertsIfInsufficientConfirmations() public { + testSubmitTransactionIncrement(); + + uint256 txId = 0; + confirmTransaction(address(1002), txId); + confirmTransaction(address(1003), txId); + + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + + vm.prank(address(1001)); + multiSig.executeTransaction(txId); + } + + /// @dev Helper function to build calldata to remove a single owner via multisig and execute it. + /// @dev Submits from owner1 (implicit confirmation) then confirms with the three given confirmers before executing. + function removeOwnerViaMultiSig(address _ownerToRemove, uint256 _txIndex, address _confirmer1, address _confirmer2, address _confirmer3) private { + address[] memory ownersToRemove = new address[](1); + ownersToRemove[0] = _ownerToRemove; + bytes memory data = abi.encodeCall(MultiSignatureWallet.removeOwners, (ownersToRemove)); + submitTransactionToMultiSig(data); + + confirmTransaction(_confirmer1, _txIndex); + confirmTransaction(_confirmer2, _txIndex); + confirmTransaction(_confirmer3, _txIndex); + + vm.prank(address(1001)); + multiSig.executeTransaction(_txIndex); + } + + /// @dev Test to ensure 'hasValidNumberOfConfirmations' reverts if the transaction does not exist. + function testHasValidNumberOfConfirmationsRevertsIfTxDoesNotExist() public { + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.hasValidNumberOfConfirmations(0); + } + + /// @dev Test to ensure 'hasValidNumberOfConfirmations' stops counting confirmations from an owner once removed, + /// @dev even though those confirmations were valid at the time they were given. + function testHasValidNumberOfConfirmationsExcludesRemovedOwner() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + + confirmTransaction(address(1002), 0); + confirmTransaction(address(1003), 0); + confirmTransaction(address(1004), 0); + assertTrue(multiSig.hasValidNumberOfConfirmations(0)); // 4 confirmations, 4 required + + // Remove owner(1002), one of the addresses that confirmed txId 0, via a second multisig transaction. + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + + // owner(1002)'s earlier confirmation of txId 0 must no longer count: 3 valid confirmations remain, 4 required. + assertFalse(multiSig.hasValidNumberOfConfirmations(0)); + } + + /// @dev Test to ensure 'executeTransaction' reverts if a confirming owner is removed after confirming, + /// @dev dropping the number of *currently valid* confirmations below the required threshold. + /// @dev This guards against relying on the stale 'numConfirmations' counter, which is never decremented on owner removal. + function testExecuteTransactionRevertsIfConfirmingOwnerRemovedAfterConfirmation() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + + confirmTransaction(address(1002), 0); + confirmTransaction(address(1003), 0); + confirmTransaction(address(1004), 0); + + // Remove owner(1002), one of the addresses that confirmed txId 0, via a second multisig transaction. + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + + vm.prank(address(1001)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'executeTransaction' still succeeds if an owner who did NOT confirm the transaction + /// @dev is removed, since the remaining valid confirmations still satisfy the required threshold. + function testExecuteTransactionSucceedsIfNonConfirmingOwnerRemoved() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + + confirmTransaction(address(1002), 0); + confirmTransaction(address(1003), 0); + confirmTransaction(address(1004), 0); + // owner(1005) never confirms txId 0. + + // Remove owner(1005), which did not confirm txId 0, via a second multisig transaction. + removeOwnerViaMultiSig(address(1005), 1, address(1002), address(1003), address(1004)); + + vm.prank(address(1001)); + multiSig.executeTransaction(0); + + assertEq(multiSig.txCount(), 0); + assertEq(counter.counter(), 1); + } + + /// @dev Helper function that returns calldata to transfer ownership. + function dataToTransferOwnership() private view returns (bytes memory) { + return abi.encodeCall(OwnableUpgradeable.transferOwnership, (alice)); + } + + /// @dev Test to ensure ownership transfer works correctly. + function testChangeOwnership() public { + submitTransaction(dataToTransferOwnership()); + grantSufficientConfirmations(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(counter.owner(), alice); + } + + /// @dev Helper function to return calldata to add an owner in multisig. + function dataToAddOwnerInMultiSig() private returns (bytes memory) { + newOwners.push(address(5001)); + return abi.encodeCall(MultiSignatureWallet.addOwners, (newOwners)); + } + + /// @dev Helper function to submit a transaction to perform an action in the MultiSignatureWallet. + function submitTransactionToMultiSig(bytes memory _data) private { + vm.prank(address(1001)); + multiSig.submitTransaction( + address(multiSig), + 0, + 10000, + _data + ); + } + + /// @dev Test to ensure 'addOwners' adds an array of owners in multisig. + function testAddOwners() public { + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + grantSufficientConfirmations(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + address[] memory updatedOwners = multiSig.getOwners(); + assertEq(updatedOwners[5], newOwners[0]); + assertEq(multiSig.getOwners().length, 6); + } + + /// @dev Test to ensure 'addOwners' reverts if array of owners is empty. + function testAddOwnersRevertsIfOwnersArrayEmpty() public { + address[] memory emptyOwners; + bytes memory data = abi.encodeCall(MultiSignatureWallet.addOwners, (emptyOwners)); + submitTransactionToMultiSig(data); + + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'addOwners' reverts if any of the owners is address(0). + function testAddOwnersRevertsIfOwnerAddressZero() public { + newOwners.push(address(0)); + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'addOwners' reverts if caller is not an owner. + function testAddOwnersRevertsIfCallerNotOwner() public { + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + + vm.prank(alice); // Not an owner + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'addOwners' transaction reverts on execute if it has expired. + function testAddOwnersRevertsIfExpired() public { + vm.warp(500); + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(0); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'addOwners' reverts if transaction has insufficient number of confirmations. + function testAddOwnersRevertsIfInsufficientConfirmations() public { + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + + uint256 txId = 0; + confirmTransaction(address(1004), txId); + confirmTransaction(address(1005), txId); + + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(txId); + } + + /// @dev Test to ensure 'addOwners' emits only the addresses actually added, trimming out + /// no-op entries (already-owner addresses) rather than padding the event with address(0). + function testAddOwnersEmitsOnlyActuallyAddedOwners() public { + address[] memory toAdd = new address[](2); + toAdd[0] = address(1001); // already an owner - no-op + toAdd[1] = address(5001); // new + submitTransactionToMultiSig(abi.encodeCall(MultiSignatureWallet.addOwners, (toAdd))); + grantSufficientConfirmations(0); + + address[] memory expected = new address[](1); + expected[0] = address(5001); + + vm.expectEmit(false, false, false, true); + emit IMultiSignatureWallet.OwnersAdded(expected); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Helper function to return calldata to remove an array of owners from multisig. + function dataToRemoveOwnerFromMultiSig() private returns (bytes memory) { + newOwners.push(address(1001)); + return abi.encodeCall(MultiSignatureWallet.removeOwners, (newOwners)); + } + + /// @dev Test to ensure 'removeOwners' removes an array of owners from multisig. + function testRemoveOwners() public { + testAddOwners(); + + submitTransactionToMultiSig(dataToRemoveOwnerFromMultiSig()); + grantSufficientConfirmations(1); + + vm.prank(address(1002)); + multiSig.executeTransaction(1); + + assertEq(multiSig.getOwners().length, 4); + } + + /// @dev Test to ensure 'removeOwners' emits only the addresses actually removed, trimming out + /// no-op entries (non-owner addresses) rather than padding the event with address(0). + function testRemoveOwnersEmitsOnlyActuallyRemovedOwners() public { + address[] memory toRemove = new address[](2); + toRemove[0] = address(9999); // not an owner - no-op + toRemove[1] = address(1001); // real owner + submitTransactionToMultiSig(abi.encodeCall(MultiSignatureWallet.removeOwners, (toRemove))); + grantSufficientConfirmations(0); + + address[] memory expected = new address[](1); + expected[0] = address(1001); + + vm.expectEmit(false, false, false, true); + emit IMultiSignatureWallet.OwnersRemoved(expected); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'removeOwners' reverts if array of owners is empty. + function testRemoveOwnersRevertsIfOwnersArrayEmpty() public { + address[] memory emptyOwners; + bytes memory data = abi.encodeCall(MultiSignatureWallet.removeOwners, (emptyOwners)); + submitTransactionToMultiSig(data); + + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'removeOwners' reverts if number of owners goes below the number of confirmations required. + function testRemoveOwnersRevertsIfNumOfOwnersGoesBelowNumConfirmations() public { + newOwners.push(address(1003)); + newOwners.push(address(1004)); + newOwners.push(address(1005)); + + bytes memory data = abi.encodeCall(MultiSignatureWallet.removeOwners, (newOwners)); + submitTransactionToMultiSig(data); + + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'removeOwners' reverts if caller is not an owner. + function testRemoveOwnersRevertsIfnotOwner() public { + testAddOwners(); + + submitTransactionToMultiSig(dataToRemoveOwnerFromMultiSig()); + + grantSufficientConfirmations(1); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + + vm.prank(alice); // Not an owner + multiSig.executeTransaction(1); + } + + /// @dev Test to ensure 'removeOwners' transaction reverts on execute if it has expired. + function testRemoveOwnersRevertsIfExpired() public { + testAddOwners(); + + vm.warp(500); + submitTransactionToMultiSig(dataToRemoveOwnerFromMultiSig()); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(1); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(1); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'removeOwners' reverts if transaction has insufficient number of confirmations. + function testRemoveOwnersRevertsIfInsufficientConfirmations() public { + testAddOwners(); + + submitTransactionToMultiSig(dataToRemoveOwnerFromMultiSig()); + + uint256 txId = 1; + confirmTransaction(address(1004), txId); + confirmTransaction(address(1005), txId); + + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(txId); + } + + /// @dev Helper function to return calldata to update the number of confirmations required in the multisig. + function dataToUpdateNumConfimationsMultiSig(uint256 _num) private pure returns (bytes memory) { + return abi.encodeCall(MultiSignatureWallet.updateNumConfirmations, (_num)); + } + + /// @dev Test to ensure 'updateNumConfirmations' updates the number of confirmations required. + function testUpdateNumConfimations() public { + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + grantSufficientConfirmations(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(multiSig.numConfirmationsRequired(), 3); + } + + /// @dev Test to ensure 'updateNumConfirmations' reverts if the number of confirmations required is zero. + function testUpdateNumConfimationsRevertsIfNumConfirmationsZero() public { + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(0)); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'updateNumConfirmations' reverts if the number of confirmations required is more than the number of owners. + function testUpdateNumConfimationsRevertsIfNumConfirmationsMoreThanOwners() public { + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(6)); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'updateNumConfirmations' reverts if the caller is not an owner. + function testUpdateNumConfimationsRevertsIfNotOwner() public { + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + + vm.prank(alice); // Not an owner + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'updateNumConfirmations' transaction reverts on execute if it has expired. + function testUpdateNumConfimationsRevertsIfExpired() public { + vm.warp(500); + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(0); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'updateNumConfirmations' reverts if the transaction has insufficient number of confirmations. + function testUpdateNumConfimationsRevertsIfInsufficientConfirmations() public { + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + + uint256 txId = 0; + confirmTransaction(address(1002), txId); + confirmTransaction(address(1003), txId); + + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(txId); + } + + + /// @dev Test to ensure 'upgradeTo' upgrades the implementation address of the beacon. + function testUpgradeBeacon() public { + MultiSignatureWallet implV2 = new MultiSignatureWallet(); + bytes memory data = abi.encodeWithSelector(UpgradeableBeacon.upgradeTo.selector, address(implV2)); + + vm.prank(address(1001)); + multiSig.submitTransaction( + address(beacon), + 0, + 100000, + data + ); + + grantSufficientConfirmations(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(beacon.implementation(), address(implV2)); + assertNotEq(beacon.implementation(), multiSigImplV1); + } + + /// @dev Test to ensure 'upgradeTo' reverts if caller is not the owner. + function testUpgradeBeaconRevertIfNotOwner() public { + MultiSignatureWallet implV2 = new MultiSignatureWallet(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + beacon.upgradeTo(address(implV2)); + } + + /// @dev Helper function to submit a transaction for contract deployment and grant sufficient confirmations. + function submitToDeploy(bytes memory _creationCode, uint256 _value, uint256 _txIndex) private { + bytes memory data = abi.encodeCall(MultiSignatureWallet.deployContract, (_creationCode, _value)); + submitTransactionToMultiSig(data); + grantSufficientConfirmations(_txIndex); + } + + /// @dev Helper function that returns creation code to deploy ERC1967 proxy contract. + function proxyCreationCode(address _impl) private view returns (bytes memory) { + bytes memory initData = abi.encodeCall(Counter.initialize, (address(multiSig))); + + return abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode(_impl, initData) + ); + } + + /// @dev Test to ensure 'deployContract' deploys contract and assigns MultiSig as contract owner. + function testDeployContract() public { + // Deploy implementation + submitToDeploy(type(Counter).creationCode, 0, 0); + + vm.prank(address(1002)); + bytes memory dataImpl = multiSig.executeTransaction(0); + address impl = abi.decode(dataImpl, (address)); + + + // Deploy proxy + bytes memory creationCode = proxyCreationCode(impl); + submitToDeploy(creationCode, 0, 1); + + vm.prank(address(1002)); + bytes memory dataProxy = multiSig.executeTransaction(1); + address proxy = abi.decode(dataProxy, (address)); + assertEq(Counter(proxy).owner(), address(multiSig)); + } + + /// @dev Test to ensure 'deployContract' reverts if caller is not MultiSig itself. + function testDeployContractRevertsIfCallerNotMultiSig() public { + bytes memory creationCode = type(Counter).creationCode; + + vm.expectRevert(IMultiSignatureWallet.OnlyMultisigAccountCanCall.selector); + + vm.prank(alice); + multiSig.deployContract(creationCode, 0); + } + + /// @dev Test to ensure 'deployContract' reverts if contract creation code is empty. + function testDeployContractRevertsIfCreationCodeEmpty() public { + // Deploy implementation + submitToDeploy("", 0, 0); // Empty creation code + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'deployContract' reverts if initialize function is non-payable. + function testDeployContractRevertsIfInitializerNonPayable() public { + vm.deal(address(multiSig), 4 ether); + + // Deploy implementation + submitToDeploy(type(Counter).creationCode, 0, 0); + + vm.prank(address(1002)); + bytes memory dataImpl = multiSig.executeTransaction(0); + address impl = abi.decode(dataImpl, (address)); + + + // Deploy proxy + bytes memory creationCode = proxyCreationCode(impl); + submitToDeploy(creationCode, 1 ether, 1); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(1); + } + + /// @dev Test to ensure 'deployContract' reverts if creation code is invalid. + function testDeployContractRevertsIfInvalidCreationCode() public { + // Deploy implementation + submitToDeploy(hex"f1", 0, 0); // Invalid creation code + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'receive' works correctly. + function testReceive() public { + assertEq(address(multiSig).balance, 0); + + // Send ETH to multisig + vm.prank(alice); + (bool success, ) = address(multiSig).call{value: 1 ether}(""); + assertTrue(success); + + assertEq(address(multiSig).balance, 1 ether); + } + + /// @dev Test to ensure 'receive' emits event 'Deposit'. + function testReceiveEmitsEvent() public { + vm.expectEmit(true, false, false, true); + emit IMultiSignatureWallet.Deposit(alice, 1 ether, 1 ether); + + testReceive(); + } + + /// @dev Test to ensure 'getTransaction' reverts if transaction does not exist. + function testGetTransactionRevertsIfTxDoesNotExist() public { + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.getTransaction(0); + } + + /// @dev Test to ensure 'getTransaction' reverts for an expired-but-unswept transaction. + function testGetTransactionRevertsIfTxExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + multiSig.getTransaction(0); + } + + /// @dev Test to ensure 'getNextTransactionIndex' returns correct value. + function testGetNextTransactionIndex() public { + assertEq(multiSig.getNextTransactionIndex(), 0); + + testSubmitTransactionIncrement(); + + assertEq(multiSig.getNextTransactionIndex(), 1); + } + + /// @dev Test to ensure 'isConfirmed' returns true for an owner who confirmed. + function testIsConfirmedReturnsTrueForConfirmer() public { + testSubmitTransactionIncrement(); + assertTrue(multiSig.isConfirmed(0, address(1001))); + } + + /// @dev Test to ensure 'isConfirmed' returns false for a valid owner who did not confirm. + function testIsConfirmedReturnsFalseForNonConfirmer() public { + testSubmitTransactionIncrement(); + assertFalse(multiSig.isConfirmed(0, address(1002))); + } + + /// @dev Test to ensure 'isConfirmed' reverts for a non-owner. + function testIsConfirmedRevertsIfNotOwner() public { + testSubmitTransactionIncrement(); + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + multiSig.isConfirmed(0, alice); + } + + /// @dev Test to ensure 'isConfirmed' reverts for a non-owner before checking tx existence. + function testIsConfirmedRevertsIfNotOwnerNonExistentTx() public { + vm.expectRevert(IMultiSignatureWallet.NotAnOwner.selector); + multiSig.isConfirmed(99, alice); + } + + /// @dev Test to ensure 'isConfirmed' still reverts for valid owner if tx does not exist. + function testIsConfirmedRevertsIfTxDoesNotExistForOwner() public { + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.isConfirmed(99, address(1001)); + } + + /// @dev Test to ensure 'getTransaction' returns valid confirmations excluding removed owners. + function testGetTransactionReturnsValidCountAfterOwnerRemoval() public { + testSubmitTransactionIncrement(); + grantSufficientConfirmations(0); + + ( , , uint24 confsBefore, , ) = multiSig.getTransaction(0); + assertEq(confsBefore, 4); + + removeOwnerViaMultiSig(address(1004), 1, address(1002), address(1003), address(1005)); + + ( , , uint24 confsAfter, , ) = multiSig.getTransaction(0); + assertEq(confsAfter, 3); + } + + /// @dev Test to ensure 'hasValidNumberOfConfirmations' returns true when enough valid confirmations exist. + function testHasValidNumberOfConfirmationsReturnsTrue() public { + testSubmitTransactionIncrement(); + grantSufficientConfirmations(0); + assertTrue(multiSig.hasValidNumberOfConfirmations(0)); + } + + /// @dev Test to ensure 'hasValidNumberOfConfirmations' returns false when valid confirmations fall below threshold. + function testHasValidNumberOfConfirmationsReturnsFalse() public { + testSubmitTransactionIncrement(); + confirmTransaction(address(1002), 0); + confirmTransaction(address(1003), 0); + assertFalse(multiSig.hasValidNumberOfConfirmations(0)); + } + + /// @dev Test to ensure 'isConfirmed' reverts for an expired-but-unswept transaction. + function testIsConfirmedRevertsIfTxExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + multiSig.isConfirmed(0, address(1001)); + } + + /// @dev Test to ensure 'hasValidNumberOfConfirmations' reverts for an expired-but-unswept transaction. + function testHasValidNumberOfConfirmationsRevertsIfTxExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectRevert(IMultiSignatureWallet.TransactionAlreadyExpired.selector); + multiSig.hasValidNumberOfConfirmations(0); + } + + // ── issue #3448: owner-removal confirmation handling ──────────────────────────────────────── + + /// @dev Helper function to build calldata to add a single owner via multisig and execute it. + function addOwnerViaMultiSig(address _ownerToAdd, uint256 _txIndex, address _confirmer1, address _confirmer2, address _confirmer3) private { + address[] memory ownersToAdd = new address[](1); + ownersToAdd[0] = _ownerToAdd; + bytes memory data = abi.encodeCall(MultiSignatureWallet.addOwners, (ownersToAdd)); + submitTransactionToMultiSig(data); + + confirmTransaction(_confirmer1, _txIndex); + confirmTransaction(_confirmer2, _txIndex); + confirmTransaction(_confirmer3, _txIndex); + + vm.prank(address(1001)); + multiSig.executeTransaction(_txIndex); + } + + /// @dev Test to ensure a re-added owner's earlier confirmation is not counted. + function testReAddedOwnerEarlierConfirmationIsNotCounted() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); // 2 valid confirmations: 1001, 1002 + + // Remove owner(1002) via a second multisig transaction (txId 1). + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + assertFalse(multiSig.hasValidNumberOfConfirmations(0)); // only 1001 remains valid, 4 required + + // Re-add owner(1002) via a third multisig transaction (txId 2). + addOwnerViaMultiSig(address(1002), 2, address(1003), address(1004), address(1005)); + + // owner(1002)'s pre-removal confirmation of txId 0 must not be counted. + (, , uint24 confs, , ) = multiSig.getTransaction(0); + assertEq(confs, 1); + assertFalse(multiSig.isConfirmed(0, address(1002))); + assertFalse(multiSig.hasValidNumberOfConfirmations(0)); + } + + /// @dev Test to ensure a re-added owner is not permanently locked out - they can freely re-confirm. + function testReAddedOwnerCanFreshlyReConfirm() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); + + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + addOwnerViaMultiSig(address(1002), 2, address(1003), address(1004), address(1005)); + + confirmTransaction(address(1002), 0); // fresh re-confirmation + + (, , uint24 confs, , ) = multiSig.getTransaction(0); + assertEq(confs, 2); + assertTrue(multiSig.isConfirmed(0, address(1002))); + } + + /// @dev Test to ensure repeated remove/re-add cycles for the same owner keep requiring fresh confirmation each time. + function testRepeatedRemoveReAddCyclesKeepRequiringFreshConfirmation() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); + + removeOwnerViaMultiSig(address(1002), 1, address(1003), address(1004), address(1005)); + addOwnerViaMultiSig(address(1002), 2, address(1003), address(1004), address(1005)); + confirmTransaction(address(1002), 0); // fresh confirmation, 1st cycle + + removeOwnerViaMultiSig(address(1002), 3, address(1003), address(1004), address(1005)); + addOwnerViaMultiSig(address(1002), 4, address(1003), address(1004), address(1005)); + + // The 1st cycle's fresh confirmation must not survive the 2nd removal either. + assertFalse(multiSig.isConfirmed(0, address(1002))); + (, , uint24 confs, , ) = multiSig.getTransaction(0); + assertEq(confs, 1); + } + + // ── issue #3448: threshold-decrease confirmation handling ─────────────────────────────────── + + /// @dev Test to ensure execution still requires confirmations gathered under the current threshold after it is lowered. + function testExecutionRequiresFreshConfirmationsAfterThresholdLowered() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); // 2 of 4 required - insufficient + + // Lower the threshold to 2 via a second multisig transaction (txId 1), gathering the + // full original threshold's worth of confirmations to pass it. + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(2)); + grantSufficientConfirmations(1); + vm.prank(address(1001)); + multiSig.executeTransaction(1); + assertEq(multiSig.numConfirmationsRequired(), 2); + + // txId 0 must still require confirmations gathered under the new threshold. + vm.expectRevert(IMultiSignatureWallet.NotEnoughConfirmation.selector); + vm.prank(address(1001)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure a transaction can still execute after fresh re-confirmation following a threshold decrease. + function testAfterThresholdDecreaseFreshReconfirmationAllowsExecution() public { + testSubmitTransactionIncrement(); // txId 0, implicitly confirmed by owner1 (address(1001)) + confirmTransaction(address(1002), 0); + + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(2)); + grantSufficientConfirmations(1); + vm.prank(address(1001)); + multiSig.executeTransaction(1); + + // Fresh re-confirmation under the new threshold (2), including the original submitter. + confirmTransaction(address(1001), 0); + confirmTransaction(address(1002), 0); + + vm.prank(address(1001)); + multiSig.executeTransaction(0); + + assertEq(multiSig.txCount(), 0); + assertEq(counter.counter(), 1); + } + + /// @dev Test to ensure raising the threshold does not wipe unrelated pending confirmations. + function testRaisingThresholdDoesNotWipeConfirmations() public { + // Lower the threshold first so there is room to raise it again without exceeding the owner count. + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(2)); + grantSufficientConfirmations(0); + vm.prank(address(1001)); + multiSig.executeTransaction(0); + assertEq(multiSig.numConfirmationsRequired(), 2); + + // Submit and confirm a fresh, unrelated transaction (txId 1) under the new, lower threshold. + submitTransaction(dataForIncrement()); + confirmTransaction(address(1002), 1); // 2 of 2 required - sufficient + + // Raise the threshold back to 3 via a third multisig transaction (txId 2). + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + confirmTransaction(address(1002), 2); + vm.prank(address(1001)); + multiSig.executeTransaction(2); + assertEq(multiSig.numConfirmationsRequired(), 3); + + // txId 1's confirmations must be unaffected by the threshold increase. + (, , uint24 confs, , ) = multiSig.getTransaction(1); + assertEq(confs, 2); + } + + // ── cancelTransaction ──────────────────────────────────────────────────────────────────── + + /// @dev Helper function to return calldata to cancel a transaction in the multisig. + function dataToCancelTransaction(uint256 _txIndex) private pure returns (bytes memory) { + return abi.encodeCall(MultiSignatureWallet.cancelTransaction, (_txIndex)); + } + + /// @dev Test to ensure 'cancelTransaction' removes a pending transaction via multisig consensus. + function testCancelTransaction() public { + testSubmitTransactionIncrement(); // txId 0 + confirmTransaction(address(1002), 0); // stuck at 2 of 4 required + + submitTransactionToMultiSig(dataToCancelTransaction(0)); + grantSufficientConfirmations(1); + + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionCancelled(0); + + vm.prank(address(1001)); + multiSig.executeTransaction(1); + + assertEq(multiSig.txCount(), 0); + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.getTransaction(0); + } + + /// @dev Test to ensure 'cancelTransaction' reverts if caller is not the multisig itself. + function testCancelTransactionRevertsIfCallerNotMultiSig() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.OnlyMultisigAccountCanCall.selector); + vm.prank(address(1001)); + multiSig.cancelTransaction(0); + } + + /// @dev Test to ensure a transaction cancelling itself fails harmlessly: the outer + /// 'executeTransaction' already deletes it before the nested self-call can run. + function testCancelTransactionRevertsIfCancellingItself() public { + submitTransactionToMultiSig(dataToCancelTransaction(0)); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + vm.prank(address(1001)); + multiSig.executeTransaction(0); + } + + // ── removeExpiredTransaction ───────────────────────────────────────────────────────────── + + /// @dev Test to ensure 'removeExpiredTransaction' is callable by anyone once a transaction has expired. + function testRemoveExpiredTransaction() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + vm.prank(alice); // not an owner - removal is permissionless + multiSig.removeExpiredTransaction(0); + + assertEq(multiSig.txCount(), 0); + } + + /// @dev Test to ensure 'removeExpiredTransaction' reverts if the transaction has not actually expired. + function testRemoveExpiredTransactionRevertsIfNotExpired() public { + testSubmitTransactionIncrement(); + + vm.expectRevert(IMultiSignatureWallet.NotExpired.selector); + multiSig.removeExpiredTransaction(0); + } + + /// @dev Test to ensure 'removeExpiredTransaction' reverts if the transaction does not exist. + function testRemoveExpiredTransactionRevertsIfTxDoesNotExist() public { + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.removeExpiredTransaction(0); + } + + // ── maxTimeoutDuration ─────────────────────────────────────────────────────────────────── + + /// @dev Test to ensure 'maxTimeoutDuration' defaults to 30 days after initialize. + function testMaxTimeoutDurationDefault() public view { + assertEq(multiSig.maxTimeoutDuration(), 30 days); + } + + /// @dev Test to ensure 'submitTransaction' succeeds at exactly the max timeout duration. + function testSubmitTransactionSucceedsAtMaxTimeoutDuration() public { + vm.prank(address(1001)); + multiSig.submitTransaction(address(counter), 0, uint64(30 days), dataForIncrement()); + assertEq(multiSig.txCount(), 1); + } + + /// @dev Test to ensure 'submitTransaction' reverts if the timeout duration exceeds the max. + function testSubmitTransactionRevertsIfTimeoutExceedsMax() public { + vm.expectRevert(IMultiSignatureWallet.TimeoutTooLong.selector); + + vm.prank(address(1001)); + multiSig.submitTransaction(address(counter), 0, uint64(30 days) + 1, dataForIncrement()); + } + + /// @dev Helper function to return calldata to update the max timeout duration in the multisig. + function dataToUpdateMaxTimeoutDuration(uint64 _newMax) private pure returns (bytes memory) { + return abi.encodeCall(MultiSignatureWallet.updateMaxTimeoutDuration, (_newMax)); + } + + /// @dev Test to ensure 'updateMaxTimeoutDuration' updates the cap via multisig consensus. + function testUpdateMaxTimeoutDuration() public { + submitTransactionToMultiSig(dataToUpdateMaxTimeoutDuration(uint64(7 days))); + grantSufficientConfirmations(0); + + vm.expectEmit(false, false, false, true); + emit IMultiSignatureWallet.MaxTimeoutDurationUpdated(uint64(7 days)); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(multiSig.maxTimeoutDuration(), 7 days); + } + + /// @dev Test to ensure 'updateMaxTimeoutDuration' reverts if given a zero duration. + function testUpdateMaxTimeoutDurationRevertsIfZero() public { + submitTransactionToMultiSig(dataToUpdateMaxTimeoutDuration(0)); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + } + + /// @dev Test to ensure 'updateMaxTimeoutDuration' reverts if caller is not the multisig itself. + function testUpdateMaxTimeoutDurationRevertsIfCallerNotMultiSig() public { + vm.expectRevert(IMultiSignatureWallet.OnlyMultisigAccountCanCall.selector); + + vm.prank(alice); + multiSig.updateMaxTimeoutDuration(uint64(7 days)); + } + + // ── MultisigBeacon two-step ownership ──────────────────────────────────────────────────── + + /// @dev Helper function to submit a transaction targeting the beacon itself (e.g. upgradeTo, transferOwnership). + function submitTransactionToBeacon(bytes memory _data) private { + vm.prank(address(1001)); + multiSig.submitTransaction( + address(beacon), + 0, + 10000, + _data + ); + } + + /// @dev Test to ensure 'transferOwnership' sets a pending owner without changing the current owner. + function testBeaconTransferOwnershipSetsPendingOwner() public { + submitTransactionToBeacon(abi.encodeWithSelector(beacon.transferOwnership.selector, alice)); + grantSufficientConfirmations(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(beacon.pendingOwner(), alice); + assertEq(beacon.owner(), address(multiSig)); + } + + /// @dev Test to ensure 'acceptOwnership' reverts if called by anyone other than the pending owner. + function testBeaconAcceptOwnershipRevertsIfNotPendingOwner() public { + testBeaconTransferOwnershipSetsPendingOwner(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, address(this))); + beacon.acceptOwnership(); + } + + /// @dev Test to ensure a transfer to the wrong address is recoverable: the original owner + /// retains control until the transfer is explicitly accepted, and can issue a corrective + /// transfer instead. + function testBeaconOwnershipTransferMistakeIsRecoverable() public { + address wrongAddress = address(0xBAD); + submitTransactionToBeacon(abi.encodeWithSelector(beacon.transferOwnership.selector, wrongAddress)); + grantSufficientConfirmations(0); + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + // Ownership has NOT actually moved yet - the multisig still controls the beacon. + assertEq(beacon.owner(), address(multiSig)); + MultiSignatureWallet implV2 = new MultiSignatureWallet(); + bytes memory upgradeData = abi.encodeWithSelector(UpgradeableBeacon.upgradeTo.selector, address(implV2)); + submitTransactionToBeacon(upgradeData); + grantSufficientConfirmations(1); + vm.prank(address(1002)); + multiSig.executeTransaction(1); + assertEq(beacon.implementation(), address(implV2)); + + // Issue a corrective transfer to the intended address instead. + submitTransactionToBeacon(abi.encodeWithSelector(beacon.transferOwnership.selector, alice)); + grantSufficientConfirmations(2); + vm.prank(address(1002)); + multiSig.executeTransaction(2); + assertEq(beacon.pendingOwner(), alice); + assertEq(beacon.owner(), address(multiSig)); + } + + /// @dev Test to ensure 'renounceOwnership' is disabled on the beacon, since giving up + /// ownership would leave 'upgradeTo' permanently unreachable. + function testBeaconRenounceOwnershipReverts() public { + submitTransactionToBeacon(abi.encodeCall(MultisigBeacon.renounceOwnership, ())); + grantSufficientConfirmations(0); + + vm.expectRevert(IMultiSignatureWallet.ExecutionFailed.selector); + vm.prank(address(1002)); + multiSig.executeTransaction(0); + + assertEq(beacon.owner(), address(multiSig)); + } +} diff --git a/solidity/supra_contracts/test/RegistryFacet.t.sol b/solidity/supra_contracts/test/RegistryFacet.t.sol new file mode 100644 index 0000000000..b9f726e1bc --- /dev/null +++ b/solidity/supra_contracts/test/RegistryFacet.t.sol @@ -0,0 +1,1956 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {BaseDiamondTest, FailingERC20} from "./BaseDiamondTest.t.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {TaskMetadata} from "../src/libraries/LibAppStorage.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; + +contract RegistryFacetTest is BaseDiamondTest { + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'register' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'register' reverts if automation is not enabled. + function testRegisterRevertsIfAutomationNotEnabled() public { + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, // payload + predicate, // predicate + uint64(block.timestamp + 1250), // expiryTime + uint128(100_000), // maxGasAmount + uint128(4 gwei), // gasPriceCap + uint128(60.1 ether), // automationFeeCapForCycle + 0, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'register' reverts if registration is disabled. + function testRegisterRevertsIfRegistrationDisabled() public { + // Disable registration + vm.prank(admin); + IConfigFacet(diamondAddr).disableRegistration(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.RegistrationDisabled.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, // payload + predicate, // predicate + uint64(block.timestamp + 1250), // expiryTime + uint128(100_000), // maxGasAmount + uint128(4 gwei), // gasPriceCap + uint128(60.1 ether), // automationFeeCapForCycle + 0, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'register' reverts if predicate target address is zero. + function testRegisterRevertsIfPredicateTargetZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + // Create predicate with address(0) as target + bytes memory predicate = createPredicate(address(0)); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate target address is EOA. + function testRegisterRevertsIfPredicateTargetEoa() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + // Create predicate with EOA as target address + bytes memory predicate = createPredicate(alice); + + vm.expectRevert(LibUtils.AddressCannotBeEOA.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate payload is empty. + function testRegisterRevertsIfPredicatePayloadEmpty() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + bytes memory predicate = abi.encode(diamondAddr, bytes("")); + + vm.expectRevert(IRegistryFacet.InvalidPayloadLength.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate payload is too short. + function testRegisterRevertsIfPredicatePayloadTooShort() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + bytes memory invalidPayload = hex"1234"; // 2 bytes + + bytes memory predicate = abi.encode(diamondAddr, invalidPayload); + + vm.expectRevert(IRegistryFacet.InvalidPayloadLength.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate updates state. + function testRegisterRevertsIfPredicateUpdatesState() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + // Create predicate that updates state + bytes memory invalidPredicate = abi.encode( + diamondAddr, abi.encodeCall(IRegistryFacet.register, ( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + )) + ); + + vm.expectRevert(IRegistryFacet.StaticCallToPredicateFailed.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + invalidPredicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if task capacity is reached. + function testRegisterRevertsIfTaskCapacityReached() public { + address diamond = deployCustomRegistry(); + + registerUst(diamond, 2450); + registerUst(diamond, 2450); + assertEq(IRegistryFacet(diamond).totalTasks(), 2); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamond); + + // Third registration should revert with TaskCapacityReached + vm.expectRevert(IRegistryFacet.TaskCapacityReached.selector); + + vm.prank(alice); + IRegistryFacet(diamond).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 2, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate returns invalid data length. + function testRegisterRevertsIfPredicateReturnsInvalidLength() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + // Create predicate that does not return 32 bytes + bytes memory predicate = abi.encode(diamondAddr, abi.encodeCall(ICoreFacet.getCycleInfo, ())); + + vm.expectRevert(IRegistryFacet.InvalidReturnLengthOfPredicate.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if predicate returns invalid return type. + function testRegisterRevertsIfPredicateReturnsInvalidType() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + + // Create predicate that doesn't return boolean + bytes memory predicate = abi.encode(diamondAddr, abi.encodeCall(ICoreFacet.getCycleDuration, ())); + + vm.expectRevert(IRegistryFacet.InvalidReturnTypeOfPredicate.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if expiry time is equal to or less than registration time. + function testRegisterRevertsIfInvalidExpiryTime() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidExpiryTime.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp), // Invalid expiryTime + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if task duration is greater than the task duration cap. + function testRegisterRevertsIfInvalidTaskDuration() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidTaskDuration.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + (3600 * 24 * 7) + 1), // Invalid task duration + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if task expires before the next cycle. + function testRegisterRevertsIfTaskExpiresBeforeNextCycle() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.TaskExpiresBeforeNextCycle.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1200), // Task expires before next cycle + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if payload target address is zero. + function testRegisterRevertsIfPayloadTargetZero() public { + bytes[] memory auxData; + // Invalid address: address(0) + bytes memory payload = createPayload(0, address(0), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(LibUtils.AddressCannotBeZero.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if payload calldata is empty. + function testRegisterRevertsIfPayloadCalldataEmpty() public { + bytes[] memory auxData; + + // Create payload with empty calldata + bytes memory payload = createPayload(0, address(erc20SupraHandler), bytes("")); + + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidPayloadLength.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if calldata length is less than 4 bytes. + function testRegisterRevertsIfPayloadCalldataTooShort() public { + bytes[] memory auxData; + + // Create payload with invalid calldata + bytes memory payload = createPayload(0, address(erc20SupraHandler), hex"1234"); + + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidPayloadLength.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if payload target address is EOA. + function testRegisterRevertsIfPayloadTargetEoa() public { + bytes[] memory auxData; + // Invalid address: EOA address being passed + bytes memory payload = createPayload(0, alice, abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(LibUtils.AddressCannotBeEOA.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if 0 is passed as max gas amount. + function testRegisterRevertsIfMaxGasAmountZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidMaxGasAmount.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(0), // maxGasAmount + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' rejects a near-zero max gas amount via the predicate + /// gas-starvation mitigation (distinct from the maxGasAmount==0 case above, which is caught + /// by the explicit InvalidMaxGasAmount check before validatePredicate ever runs). This proves + /// the DoS mitigation actually works: a task can't under-fund its predicate's gas and still + /// register, since validatePredicate's staticcall is gas-limited to maxGasAmount. + function testRegisterRevertsIfMaxGasAmountTooLowForPredicate() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.StaticCallToPredicateFailed.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(1), // maxGasAmount: nonzero, but too little gas for the predicate staticcall + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if 0 is passed as gas price cap. + function testRegisterRevertsIfGasPriceCapZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidGasPriceCap.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(0), // gasPriceCap + uint128(60.1 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if automation fee cap is less than the estimated automation fee. + function testRegisterRevertsIfAutomationFeeCapLessThanEstimated() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert( + abi.encodeWithSelector( + IRegistryFacet.InsufficientFeeCapForCycle.selector, + 3 ether + ) + ); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(0), // automationFeeCapForCycle + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if gas committed exceeds the registry max gas cap. + function testRegisterRevertsIfGasCommittedExceedsMaxGasCap() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.GasCommittedExceedsMaxGasCap.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(20_000_001), // Gas exceeds max gas cap + uint128(4 gwei), + uint128(6835 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts when 'transferFrom' returns false. + function testRegisterRevertsIfTransferFromFails() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + + vm.etch(address(erc20Supra), address(new FailingERC20()).code); + + vm.expectRevert(IRegistryFacet.TransferFailed.selector); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 2, + auxData + ); + vm.stopPrank(); + } + + /// @dev Test to ensure 'register' reverts if a cycle transition is in progress. + function testRegisterRevertsIfCycleTransitionInProgress() public { + registerUst(diamondAddr, 2450); + + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.CycleTransitionInProgress.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 2, + auxData + ); + } + + /// @dev Test to ensure 'register' registers a UST. + function testRegister() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 4, + auxData + ); + vm.stopPrank(); + + TaskMetadata memory taskMetadata = IRegistryFacet(diamondAddr).getTaskDetails(0); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + + uint256[] memory userTasks = IRegistryFacet(diamondAddr).getTasksByAddress(alice); + assertEq(userTasks.length, 1); + assertEq(userTasks[0], 0); + + assertEq(IRegistryFacet(diamondAddr).getNextTaskIndex(), 1); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 100_000); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 61.1 ether); + assertEq(erc20Supra.balanceOf(alice), 38.9 ether); + + assertEq(taskMetadata.maxGasAmount, 100_000); + assertEq(taskMetadata.gasPriceCap, 4 gwei); + assertEq(taskMetadata.automationFeeCapForCycle, 60.1 ether); + assertEq(taskMetadata.depositFee, 60.1 ether); + assertEq(taskMetadata.txHash, keccak256("txHash")); + assertEq(taskMetadata.taskIndex, 0); + assertEq(taskMetadata.registrationTime, uint64(block.timestamp)); + assertEq(taskMetadata.expiryTime, uint64(block.timestamp + 1250)); + assertEq(taskMetadata.priority, 0); + assertEq(uint8(taskMetadata.taskType), 0); + assertEq(uint8(taskMetadata.taskState), 0); + assertEq(taskMetadata.owner, alice); + assertEq(taskMetadata.payloadTx, payload); + assertEq(taskMetadata.predicate, predicate); + assertEq(taskMetadata.auxData, auxData); + } + + /// @dev Test to ensure 'register' emits event 'TaskRegistered'. + function testRegisterEmitsEvent() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + + TaskMetadata memory taskMetadata = TaskMetadata({ + maxGasAmount: 100_000, + gasPriceCap: 4 gwei, + automationFeeCapForCycle: 60.1 ether, + depositFee: 60.1 ether, + txHash: keccak256("txHash"), + taskIndex: 0, + registrationTime: uint64(block.timestamp), + expiryTime: uint64(block.timestamp + 1250), + priority: 0, + owner: alice, + taskType: LibCommon.TaskType.UST, + taskState: LibCommon.TaskState.PENDING, + payloadTx: payload, + predicate: predicate, + auxData: auxData + }); + + vm.expectEmit(true, true, false, true); + emit IRegistryFacet.TaskRegistered(0, alice, 1 ether, 60.1 ether, taskMetadata); + + IRegistryFacet(diamondAddr).register( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 0, + auxData + ); + vm.stopPrank(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'updateDataLengthCaps' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev payloadTx's inner callData is only length-checked (>= 4 bytes) and decoded at + /// registration time, never executed here — so zero-filled bytes are safe content, and this + /// gives exact control over `_payloadTx.length` via the abi.encode(uint128,address,bytes, + /// AccessListEntry[]) layout: total = 192 + ceil(callDataLen/32)*32 with an empty access list. + function payloadOfLength(uint256 _totalLength) internal view returns (bytes memory) { + LibCommon.AccessListEntry[] memory emptyAccessList; + bytes memory callData = new bytes(_totalLength - 192); + return abi.encode(uint128(0), address(erc20SupraHandler), callData, emptyAccessList); + } + + /// @dev Unlike payloadTx, the predicate's callData IS executed (via staticcall), so it must + /// stay a valid call to `_target`. `isRegistrationEnabled()` takes no arguments, so trailing + /// zero-padding after its 4-byte selector is harmless and gives exact control over + /// `_predicate.length` via the abi.encode(address,bytes) layout: total = 96 + ceil(callDataLen/32)*32. + function paddedPredicate(address _target, uint256 _totalLength) internal pure returns (bytes memory) { + uint256 callDataLen = _totalLength - 96; + bytes memory callData = abi.encodePacked(IConfigFacet.isRegistrationEnabled.selector, new bytes(callDataLen - 4)); + return abi.encode(_target, callData); + } + + /// @dev Test to ensure 'updateDataLengthCaps' reverts if not called by the owner. + function testUpdateDataLengthCapsRevertsIfNotOwner() public { + vm.expectRevert(); + vm.prank(alice); + IConfigFacet(diamondAddr).updateDataLengthCaps(8192, 4096, 100, 10); + } + + /// @dev Test to ensure 'updateDataLengthCaps' updates the caps and emits an event. + function testUpdateDataLengthCapsEmitsEvent() public { + vm.expectEmit(true, false, false, true); + emit IConfigFacet.DataLengthCapsUpdated(8192, 4096, 100, 10); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateDataLengthCaps(8192, 4096, 100, 10); + + (uint16 maxPayloadLength, uint16 maxPredicateLength, uint16 maxAuxDataLength, uint16 maxAuxDataEntries) = IConfigFacet(diamondAddr).getDataLengthCaps(); + assertEq(maxPayloadLength, 8192); + assertEq(maxPredicateLength, 4096); + assertEq(maxAuxDataLength, 100); + assertEq(maxAuxDataEntries, 10); + } + + /// @dev Test to ensure the default data length caps set at init match the documented defaults. + function testGetDataLengthCapsReturnsDefaults() public view { + (uint16 maxPayloadLength, uint16 maxPredicateLength, uint16 maxAuxDataLength, uint16 maxAuxDataEntries) = IConfigFacet(diamondAddr).getDataLengthCaps(); + assertEq(maxPayloadLength, 4096); + assertEq(maxPredicateLength, 2048); + assertEq(maxAuxDataLength, 0); + assertEq(maxAuxDataEntries, 0); + } + + /// @dev Test to ensure 'register' succeeds when payloadTx is exactly at the default cap. + function testRegisterSucceedsAtPayloadLengthCap() public { + bytes[] memory auxData; + bytes memory payload = payloadOfLength(4096); + assertEq(payload.length, 4096); + bytes memory predicate = createPredicate(diamondAddr); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + vm.stopPrank(); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure 'register' reverts when payloadTx exceeds the default cap. + function testRegisterRevertsIfPayloadExceedsCap() public { + bytes[] memory auxData; + bytes memory payload = payloadOfLength(4128); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.PayloadTooLarge.selector); + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + } + + /// @dev Test to ensure 'register' succeeds when predicate is exactly at the default cap. + function testRegisterSucceedsAtPredicateLengthCap() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = paddedPredicate(diamondAddr, 2048); + assertEq(predicate.length, 2048); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + vm.stopPrank(); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure 'register' reverts when predicate exceeds the default cap. + function testRegisterRevertsIfPredicateExceedsCap() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = paddedPredicate(diamondAddr, 2080); + + vm.expectRevert(IRegistryFacet.PredicateTooLarge.selector); + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + } + + /// @dev Test to ensure 'register' reverts on any non-empty auxData given the default + /// maxAuxDataLength of 0 — nothing consumes auxData yet, so its default contribution to + /// storage/copy cost is zero until an owner deliberately raises the cap. + function testRegisterRevertsIfAuxDataExceedsDefaultCap() public { + bytes[] memory auxData = new bytes[](1); + auxData[0] = new bytes(1); + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.AuxDataTooLarge.selector); + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + } + + /// @dev Test to ensure 'register' bounds the number of auxData entries independently of their + /// combined byte length. + function testRegisterRevertsIfAuxDataEntryCountExceedsDefaultCap() public { + bytes[] memory auxData = new bytes[](1); + auxData[0] = new bytes(0); + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.AuxDataTooLarge.selector); + vm.prank(alice); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + } + + /// @dev Test to ensure a previously-rejected auxData size succeeds once the owner raises the cap. + function testRegisterSucceedsWithAuxDataAfterOwnerRaisesCap() public { + bytes[] memory auxData = new bytes[](2); + auxData[0] = new bytes(50); + auxData[1] = new bytes(50); + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.prank(admin); + IConfigFacet(diamondAddr).updateDataLengthCaps(4096, 2048, 100, 2); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(block.timestamp + 1250), uint128(100_000), uint128(4 gwei), uint128(60.1 ether), 0, auxData + ); + vm.stopPrank(); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'registerSystemTask' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'registerSystemTask' reverts if caller is not authorized. + function testRegisterSystemTaskRevertsIfUnauthorizedCaller() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.UnauthorizedAccount.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).registerSystemTask( + payload, // payload + predicate, // predicate + uint64(block.timestamp + 1250), // expiryTime + uint128(100_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if automation is not enabled. + function testRegisterSystemTaskRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + registerGst(diamondAddr, 2450); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if registration is disabled. + function testRegisterSystemTaskRevertsIfRegistrationDisabled() public { + vm.prank(admin); + IConfigFacet(diamondAddr).disableRegistration(); + + vm.expectRevert(IRegistryFacet.RegistrationDisabled.selector); + registerGst(diamondAddr, 2450); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if system task capacity is reached. + function testRegisterSystemTaskRevertsIfSysTaskCapacityReached() public { + address diamond = deployCustomRegistry(); + + registerGst(diamond, 2450); + registerGst(diamond, 2450); + assertEq(IRegistryFacet(diamond).totalTasks(), 2); + assertEq(IRegistryFacet(diamond).totalSystemTasks(), 2); + + // Third registration should revert with TaskCapacityReached + vm.expectRevert(IRegistryFacet.TaskCapacityReached.selector); + registerGst(diamond, 2450); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if task duration is greater than system task duration cap. + function testRegisterSystemTaskRevertsIfInvalidTaskDuration() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.InvalidTaskDuration.selector); + + vm.prank(bob); + IRegistryFacet(diamondAddr).registerSystemTask( + payload, + predicate, + uint64(block.timestamp + (3600 * 24 * 180) + 1), // Invalid task duration + uint128(100_000), + 2, + auxData + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if gas committed exceeds the system registry max gas cap. + function testRegisterSystemTaskRevertsIfGasCommittedExceedsMaxGasCap() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.expectRevert(IRegistryFacet.GasCommittedExceedsMaxGasCap.selector); + + vm.prank(bob); + IRegistryFacet(diamondAddr).registerSystemTask( + payload, + predicate, + uint64(block.timestamp + 1250), + uint128(20_000_001), // Gas exceeds max gas cap + 2, + auxData + ); + } + + /// @dev Test to ensure 'registerSystemTask' registers a GST. + function testRegisterSystemTask() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.prank(bob); + IRegistryFacet(diamondAddr).registerSystemTask( + payload, // payload + predicate, // predicate + uint64(block.timestamp + 1250), // expiryTime + uint128(100_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + + TaskMetadata memory taskMetadata = IRegistryFacet(diamondAddr).getTaskDetails(0); + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertTrue(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 1); + + uint256[] memory userTasks = IRegistryFacet(diamondAddr).getTasksByAddress(bob); + assertEq(userTasks.length, 1); + assertEq(userTasks[0], 0); + + assertEq(IRegistryFacet(diamondAddr).getNextTaskIndex(), 1); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 100_000); + + assertEq(taskMetadata.maxGasAmount, 100_000); + assertEq(taskMetadata.gasPriceCap, 0); + assertEq(taskMetadata.automationFeeCapForCycle, 0); + assertEq(taskMetadata.depositFee, 0); + assertEq(taskMetadata.txHash, keccak256("txHash")); + assertEq(taskMetadata.taskIndex, 0); + assertEq(taskMetadata.registrationTime, uint64(block.timestamp)); + assertEq(taskMetadata.expiryTime, uint64(block.timestamp + 1250)); + assertEq(taskMetadata.priority, 2); + assertEq(uint8(taskMetadata.taskType), 1); + assertEq(uint8(taskMetadata.taskState), 0); + assertEq(taskMetadata.owner, bob); + assertEq(taskMetadata.payloadTx, payload); + assertEq(taskMetadata.predicate, predicate); + assertEq(taskMetadata.auxData, auxData); + } + + /// @dev Test to ensure 'registerSystemTask' emits event 'SystemTaskRegistered'. + function testRegisterSystemTaskEmitsEvent() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + TaskMetadata memory taskMetadata = TaskMetadata({ + maxGasAmount: 100_000, + gasPriceCap: 0, + automationFeeCapForCycle: 0, + depositFee: 0, + txHash: keccak256("txHash"), + taskIndex: 0, + registrationTime: uint64(block.timestamp), + expiryTime: uint64(block.timestamp + 1250), + priority: 2, + owner: bob, + taskType: LibCommon.TaskType.GST, + taskState: LibCommon.TaskState.PENDING, + payloadTx: payload, + predicate: predicate, + auxData: auxData + }); + + vm.expectEmit(true, true, false, true); + emit IRegistryFacet.SystemTaskRegistered(0, bob, block.timestamp, taskMetadata); + + vm.prank(bob); + IRegistryFacet(diamondAddr).registerSystemTask( + payload, // payload + predicate, // predicate + uint64(block.timestamp + 1250), // expiryTime + uint128(100_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'cancelTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'cancelTasks' reverts if automation is not enabled. + function testCancelTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelTasks' reverts if input array is empty. + function testCancelTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelTasks' does nothing if task does not exist. + function testCancelTasksDoesNothingIfTaskDoesNotExist() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + } + + /// @dev Test to ensure 'cancelTasks' reverts if task type is not UST. + function testCancelTasksRevertsIfTaskTypeNotUST() public { + testRegisterSystemTask(); + vm.expectRevert(IRegistryFacet.UnsupportedTaskOperation.selector); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.prank(bob); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelTasks' reverts if caller is not the task owner. + function testCancelTasksRevertsIfUnauthorizedCaller() public { + testRegister(); + vm.expectRevert(IRegistryFacet.UnauthorizedAccount.selector); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.prank(bob); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelTasks' cancels a UST. + function testCancelTasks() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getTasksByAddress(alice).length, 0); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + assertEq(erc20Supra.balanceOf(diamondAddr), 31.05 ether); + assertEq(erc20Supra.balanceOf(alice), 68.95 ether); + } + + /// @dev Test to ensure 'cancelTasks' emits event 'TasksCancelled'. + function testCancelTasksEmitsEvent() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + LibCommon.TaskCancelled[] memory cancelledTasks = new LibCommon.TaskCancelled[](1); + cancelledTasks[0] = LibCommon.TaskCancelled(0, LibCommon.TaskType.UST, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksCancelled(cancelledTasks, alice); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelTasks' emits TasksCancelled containing only the tasks actually + /// cancelled, even when the input also contains non-existent indexes. + function testCancelTasksEmitsOnlyExistingTasks() public { + testRegister(); // task 0 + registerUst(diamondAddr, 2450); // task 1 + + uint64[] memory taskIndexes = new uint64[](2); + taskIndexes[0] = 1; + taskIndexes[1] = 999; // does not exist + + LibCommon.TaskCancelled[] memory expectedCancelledTasks = new LibCommon.TaskCancelled[](1); + expectedCancelledTasks[0] = LibCommon.TaskCancelled(1, LibCommon.TaskType.UST, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksCancelled(expectedCancelledTasks, alice); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskIndexes); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure 'cancelTasks' on an already-ACTIVE task (post cycle-transition) only + /// flips its state to CANCELLED and reduces the gas committed for the next cycle, rather than + /// removing it from storage the way cancelling a PENDING task does. + function testCancelTasksSetsStateToCancelledForActiveTask() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 100_000); + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelTasks(taskUint64); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(uint8(IRegistryFacet(diamondAddr).getTaskDetails(0).taskState), uint8(LibCommon.TaskState.CANCELLED)); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 0); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'cancelSystemTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'cancelSystemTasks' reverts if automation is not enabled. + function testCancelSystemTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelSystemTasks' reverts if input array is empty. + function testCancelSystemTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelSystemTasks' does nothing if task does not exist. + function testCancelSystemTasksDoesNothingIfTaskDoesNotExist() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 1); + } + + /// @dev Test to ensure 'cancelSystemTasks' reverts if task type is not GST. + function testCancelSystemTasksRevertsIfTaskTypeNotGST() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnsupportedTaskOperation.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelSystemTasks' reverts if caller is not the task owner. + function testCancelSystemTasksRevertsIfUnauthorizedCaller() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnauthorizedAccount.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelSystemTasks' cancels a GST. + function testCancelSystemTasks() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.prank(bob); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertFalse(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).getTasksByAddress(bob).length, 0); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 0); + } + + /// @dev Test to ensure 'cancelSystemTasks' emits event 'TasksCancelled'. + function testCancelSystemTasksEmitsEvent() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + LibCommon.TaskCancelled[] memory cancelledTasks = new LibCommon.TaskCancelled[](1); + cancelledTasks[0] = LibCommon.TaskCancelled(0, LibCommon.TaskType.GST, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksCancelled(cancelledTasks, bob); + + vm.prank(bob); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'cancelSystemTasks' on an already-ACTIVE GST (post cycle-transition) + /// only flips its state to CANCELLED and reduces the sys gas committed for the next cycle, + /// rather than removing it from storage the way cancelling a PENDING task does. + function testCancelSystemTasksSetsStateToCancelledForActiveTask() public { + registerGst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 100_000); + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + vm.prank(bob); + IRegistryFacet(diamondAddr).cancelSystemTasks(taskUint64); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(uint8(IRegistryFacet(diamondAddr).getTaskDetails(0).taskState), uint8(LibCommon.TaskState.CANCELLED)); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 0); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'stopTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'stopTasks' reverts if automation is not enabled. + function testStopTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts when cycle transition is in progress. + function testStopTasksRevertsIfCycleTransitionInProgress() public { + registerUst(diamondAddr, 2450); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.expectRevert(IRegistryFacet.CycleTransitionInProgress.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if input array is empty. + function testStopTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if caller is not the task owner. + function testStopTasksRevertsIfUnauthorizedCaller() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnauthorizedAccount.selector); + + vm.prank(bob); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if task type is not UST. + function testStopTasksRevertsIfTaskTypeNotUST() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnsupportedTaskOperation.selector); + + vm.prank(bob); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' does nothing if task does not exist. + function testStopTasksDoesNothingIfTaskDoesNotExist() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + } + + /// @dev Test to ensure 'stopTasks' stops the input UST tasks. + function testStopTasks() public { + testRegister(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + vm.deal(alice, 200 ether); + vm.prank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + + processCycleTransition(diamondAddr, taskIndexes); + + assertEq(erc20Supra.balanceOf(diamondAddr), 64.1 ether); + assertEq(erc20Supra.balanceOf(alice), 135.9 ether); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getTasksByAddress(alice).length, 0); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + assertEq(erc20Supra.balanceOf(diamondAddr), 3.9375 ether); + assertEq(erc20Supra.balanceOf(alice), 196.0625 ether); + } + + /// @dev Test to ensure 'stopTasks' emits event 'TasksStopped'. + function testStopTasksEmitsEvent() public { + testRegister(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + vm.deal(alice, 200 ether); + vm.prank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + + processCycleTransition(diamondAddr, taskIndexes); + + LibCommon.TaskStopped[] memory stoppedTasks = new LibCommon.TaskStopped[](1); + stoppedTasks[0] = LibCommon.TaskStopped(0, 60.1 ether, 0.0625 ether, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksStopped(stoppedTasks, alice); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + } + + /// @dev Test to ensure 'stopTasks' emits TasksStopped containing only the tasks actually + /// stopped, even when the input also contains non-existent indexes. + function testStopTasksEmitsOnlyExistingTasks() public { + registerUst(diamondAddr, 2450); // task 0 + registerUst(diamondAddr, 2450); // task 1 + + uint64[] memory taskIndexes = new uint64[](2); + taskIndexes[0] = 1; + taskIndexes[1] = 999; // does not exist + + LibCommon.TaskStopped[] memory expectedStoppedTasks = new LibCommon.TaskStopped[](1); + expectedStoppedTasks[0] = LibCommon.TaskStopped(1, 30.05 ether, 0, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksStopped(expectedStoppedTasks, alice); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskIndexes); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + } + + /// @dev Test to ensure stopping a PENDING task refunds half the deposit. + function testStopPendingTask() public { + registerUst(diamondAddr, 2450); + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + uint256 balanceBefore = erc20Supra.balanceOf(alice); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(erc20Supra.balanceOf(alice), balanceBefore + 30.05 ether); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + } + + /// @dev Test to ensure stopping an expired task refunds the full deposit but returns 0 cycle fee. + function testStopExpiredTask() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + // Warp past expiry (task was registered with expiry time = block.timestamp + 1250) + vm.warp(block.timestamp + 1251); + + uint256 balanceBefore = erc20Supra.balanceOf(alice); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(erc20Supra.balanceOf(alice), balanceBefore + 60.1 ether); // Cycle fee refund = 0, deposit refund = 60.1 ether + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0 ether); + } + + /// @dev Locks in cycleLockedFees's current accounting behavior for a task stopped shortly + /// after its first active cycle begins, left intentionally as-is. + function testStopTasksLeavesStuckCycleLockedFeesResidualForFirstCycleShortMarginTask() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.deal(alice, 5000 ether); + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 5000 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + // Expiry is 5 seconds past cycle 1's end (1201) -- a tiny margin into cycle 2. + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(1206), uint128(5_000_000), uint128(4 gwei), uint128(1000 ether), 0, auxData + ); + vm.stopPrank(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + + // Charged a full cycle's fee at registration-to-active transition (PENDING branch). + uint256 cycleLockedFeesAfterCharge = IRegistryFacet(diamondAddr).getCycleLockedFees(); + assertEq(cycleLockedFeesAfterCharge, 150 ether); + + // Stop the task early in cycle 2 (t=1203), well before its actual expiry (1206). + vm.warp(1203); + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 149.375 ether); + } + + /// @dev Locks in that the behavior documented above persists across an empty-registry cycle + /// boundary. + function testStuckCycleLockedFeesPersistsThroughEmptyFastPathCycle() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.deal(alice, 5000 ether); + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 5000 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(1206), uint128(5_000_000), uint128(4 gwei), uint128(1000 ether), 0, auxData + ); + vm.stopPrank(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + + vm.warp(1203); + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + uint256 stuckAfterStop = IRegistryFacet(diamondAddr).getCycleLockedFees(); + assertEq(stuckAfterStop, 149.375 ether); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + + // Advance through the next (now-empty) cycle boundary. + (, uint64 start2, uint64 duration2,) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(start2 + duration2); + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (,,, LibCommon.CycleState state) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(uint8(state), uint8(LibCommon.CycleState.STARTED)); + + // Residual persists unchanged -- the empty fast path never touches cycleLockedFees. + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), stuckAfterStop); + } + + /// @dev Locks in cycleLockedFees's accounting behavior for a task that is already active (not + /// on its first cycle) and expires within the cycle in which it is stopped. + function testStopTasksUnlocksCloseToChargedFeeForAlreadyActiveTaskExpiringMidCycle() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + bytes memory predicate = createPredicate(diamondAddr); + + vm.deal(alice, 5000 ether); + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 5000 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + // Expires 5 seconds into cycle 3. + IRegistryFacet(diamondAddr).register( + payload, predicate, uint64(2406), uint128(5_000_000), uint128(4 gwei), uint128(1000 ether), 0, auxData + ); + vm.stopPrank(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + // Cycle 1 -> 2: task's first transition, survives. + processCycleTransition(diamondAddr, taskIndexes); + // Cycle 2 -> 3: task is now active and charged for its final ~5 seconds. + processCycleTransition(diamondAddr, taskIndexes); + + uint256 cycleLockedFeesAfterCharge = IRegistryFacet(diamondAddr).getCycleLockedFees(); + assertGt(cycleLockedFeesAfterCharge, 0); + + // Stop the task early in cycle 3 (t=2403), before its actual expiry (2406). + vm.warp(2403); + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + vm.prank(alice); + IRegistryFacet(diamondAddr).stopTasks(taskUint64); + + // Charge and unlock both computed min(expiry - cycle-start, cycle-duration) = 5 from the + // same inputs, so the unlock is close to (here: exactly) the charged amount -- no stuck + // residual, unlike the first-cycle PENDING case. + assertApproxEqAbs(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0, 1); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'stopSystemTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'stopSystemTasks' reverts if automation is not enabled. + function testStopSystemTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.AutomationNotEnabled.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if input array is empty. + function testStopSystemTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IRegistryFacet.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if caller is not the task owner. + function testStopSystemTasksRevertsIfUnauthorizedCaller() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnauthorizedAccount.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if task type is not GST. + function testStopSystemTasksRevertsIfTaskTypeNotGST() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IRegistryFacet.UnsupportedTaskOperation.selector); + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' does nothing if task does not exist. + function testStopSystemTasksDoesNothingIfTaskDoesNotExist() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + IRegistryFacet(diamondAddr).stopSystemTasks(taskIndexes); + + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 1); + } + + /// @dev Test to ensure 'stopSystemTasks' stops the input GST tasks. + function testStopSystemTasks() public { + testRegisterSystemTask(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + vm.prank(bob); + IRegistryFacet(diamondAddr).stopSystemTasks(taskUint64); + + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertFalse(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).getTasksByAddress(bob).length, 0); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 0); + } + + /// @dev Test to ensure 'stopSystemTasks' emits event 'TasksStopped'. + function testStopSystemTasksEmitsEvent() public { + testRegisterSystemTask(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + uint64[] memory taskUint64 = new uint64[](1); + taskUint64[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + LibCommon.TaskStopped[] memory stoppedTasks = new LibCommon.TaskStopped[](1); + stoppedTasks[0] = LibCommon.TaskStopped(0, 0, 0, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit IRegistryFacet.TasksStopped(stoppedTasks, bob); + + vm.prank(bob); + IRegistryFacet(diamondAddr).stopSystemTasks(taskUint64); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to view functions :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'getTaskIdList' returns correct task IDs. + function testGetTaskIdList() public { + registerUst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint256[] memory taskIds = IRegistryFacet(diamondAddr).getTaskIdList(); + assertEq(taskIds.length, 2); + assertEq(taskIds[0], 0); + assertEq(taskIds[1], 1); + } + + /// @dev Test to ensure 'getSystemTaskIds' returns correct system task IDs. + function testGetSystemTaskIds() public { + registerGst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint256[] memory sysTaskIds = IRegistryFacet(diamondAddr).getSystemTaskIds(); + assertEq(sysTaskIds.length, 2); + assertEq(sysTaskIds[0], 0); + assertEq(sysTaskIds[1], 1); + } + + /// @dev Test to ensure 'getTaskOwner' returns correct owner for an existing task. + function testGetTaskOwner() public { + registerUst(diamondAddr, 2450); + + address owner = IRegistryFacet(diamondAddr).getTaskOwner(0); + assertEq(owner, alice); + } + + /// @dev Test to ensure 'getTotalActiveTasks' returns the correct count of active tasks. + function testGetTotalActiveTasks() public { + registerUst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](2); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + + processCycleTransition(diamondAddr, taskIndexes); + + assertEq(IRegistryFacet(diamondAddr).getTotalActiveTasks(), 2); + } + + /// @dev Test to ensure 'getTotalActiveTasks' returns zero when no active tasks. + function testGetTotalActiveTasksZero() public view { + assertEq(IRegistryFacet(diamondAddr).getTotalActiveTasks(), 0); + } + + /// @dev Test to ensure 'getActiveTaskIds' returns correct active task IDs. + function testGetActiveTaskIds() public { + registerUst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](2); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + + processCycleTransition(diamondAddr, taskIndexes); + + uint256[] memory activeIds = IRegistryFacet(diamondAddr).getActiveTaskIds(); + assertEq(activeIds.length, 2); + assertEq(activeIds[0], 0); + assertEq(activeIds[1], 1); + } + + /// @dev Test to ensure 'getActiveTaskIds' returns empty array when no tasks are active. + function testGetActiveTaskIdsEmpty() public view { + assertEq(IRegistryFacet(diamondAddr).getActiveTaskIds().length, 0); + } + + /// @dev Test to ensure 'getTotalLockedBalance' returns the correct locked balance. + function testGetTotalLockedBalance() public { + registerUst(diamondAddr, 2450); + + assertEq(IRegistryFacet(diamondAddr).getTotalLockedBalance(), 60.1 ether); + } + + /// @dev Test to ensure 'hasActiveUserTask' returns true for an active task. + function testHasActiveUserTask() public { + registerUst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + assertTrue(IRegistryFacet(diamondAddr).hasActiveUserTask(alice, 0)); + } + + /// @dev Test to ensure 'hasActiveUserTask' returns false for a pending or non-existent task. + function testHasActiveUserTaskForPendingOrNonExistent() public { + registerUst(diamondAddr, 2450); + + assertFalse(IRegistryFacet(diamondAddr).hasActiveUserTask(alice, 0)); + assertFalse(IRegistryFacet(diamondAddr).hasActiveUserTask(alice, 99)); + } + + /// @dev Test to ensure 'hasActiveSystemTask' returns true for an active system task. + function testHasActiveSystemTask() public { + registerGst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + + processCycleTransition(diamondAddr, taskIndexes); + + assertTrue(IRegistryFacet(diamondAddr).hasActiveSystemTask(bob, 0)); + } + + /// @dev Test to ensure 'hasActiveSystemTask' returns false for a pending or non-existent system task. + function testHasActiveSystemTaskForPendingOrNonExistent() public { + registerGst(diamondAddr, 2450); + + assertFalse(IRegistryFacet(diamondAddr).hasActiveSystemTask(bob, 0)); + assertFalse(IRegistryFacet(diamondAddr).hasActiveSystemTask(bob, 99)); + } + + /// @dev Test to ensure 'hasActiveTaskOfType' returns correct values. + function testHasActiveTaskOfType() public { + registerUst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint256[] memory taskIndexes = new uint256[](2); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + + processCycleTransition(diamondAddr, taskIndexes); + + assertTrue(IRegistryFacet(diamondAddr).hasActiveTaskOfType(alice, 0, LibCommon.TaskType.UST)); + assertTrue(IRegistryFacet(diamondAddr).hasActiveTaskOfType(bob, 1, LibCommon.TaskType.GST)); + } + + /// @dev Test to ensure 'hasActiveTaskOfType' returns false for pending or non-existent task. + function testHasActiveTaskOfTypeForPendingOrNonExistent() public { + registerUst(diamondAddr, 2450); + + assertFalse(IRegistryFacet(diamondAddr).hasActiveTaskOfType(alice, 0, LibCommon.TaskType.UST)); + assertFalse(IRegistryFacet(diamondAddr).hasActiveTaskOfType(alice, 99, LibCommon.TaskType.UST)); + } + + /// @dev Test to ensure 'getTaskDetailsBulk' returns correct details for existing and non-existing tasks. + function testGetTaskDetailsBulk() public { + registerUst(diamondAddr, 2450); + registerGst(diamondAddr, 2450); + + uint64[] memory taskIndexes = new uint64[](3); + taskIndexes[0] = 0; + taskIndexes[1] = 1; + taskIndexes[2] = 99; + + TaskMetadata[] memory details = IRegistryFacet(diamondAddr).getTaskDetailsBulk(taskIndexes); + assertEq(details.length, 2); + assertEq(details[0].taskIndex, 0); + assertEq(details[0].owner, alice); + assertEq(details[1].taskIndex, 1); + assertEq(details[1].owner, bob); + } + + /// @dev Test to ensure 'calculateAutomationFeeMultiplierForCurrentCycle' returns the base fee when + /// usage is below the 50% threshold, and a higher fee when it exceeds the threshold. + function testCalculateAutomationFeeMultiplierForCurrentCycle() public { + // Scenario 1. Register a 100_000-gas task and process first cycle transition + registerUst(diamondAddr, 1250); + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + processCycleTransition(diamondAddr, taskIndexes); + + // 100_000 gas committed but below 50% threshold (10_000_000) → returns base fee + assertEq(IRegistryFacet(diamondAddr).calculateAutomationFeeMultiplierForCurrentCycle(), 0.5 ether); + + + // Scenario 2. Register a 10_500_000-gas task to push usage above the threshold + vm.deal(alice, 800 ether); + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 800 ether}(); + erc20Supra.approve(diamondAddr, type(uint256).max); + bytes[] memory auxData; + IRegistryFacet(diamondAddr).register( + createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)), + createPredicate(diamondAddr), + uint64(block.timestamp) + 1250, + uint128(10_500_000), + uint128(4 gwei), + uint128(400 ether), + 2, + auxData + ); + vm.stopPrank(); + + // Process cycle transition + ( , uint64 startTime, uint64 duration, ) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startTime + duration); + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + (uint64 index, , , ) = ICoreFacet(diamondAddr).getCycleInfo(); + uint256[] memory taskIds = new uint256[](2); + taskIds[0] = 0; + taskIds[1] = 1; + ICoreFacet(diamondAddr).processTasks(index + 1, taskIds); + vm.stopPrank(); + + // 10_500_000 gas > 50% threshold (10_000_000) → congestion fee added + assertEq(IRegistryFacet(diamondAddr).calculateAutomationFeeMultiplierForCurrentCycle(), 0.579846705 ether); + } + + /// @dev Test to ensure 'estimateAutomationFeeWithCommittedOccupancy' returns zero for zero occupancy, + /// scales linearly with task occupancy when total is below the 50% threshold, and increases + /// when gas usage pushes total above the threshold. + function testEstimateAutomationFeeWithCommittedOccupancy() public view { + // Scenario 1: Zero task occupancy → fee is zero regardless of committed occupancy + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(0, 0), 0); + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(0, 10_000_000), 0); + + // Scenario 2: Total committed gas below 50% threshold → linear scaling with task occupancy + // (100_000 + 5_000_000 = 5_100_000 < 10_000_000) → 3 ether + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(100_000, 5_000_000), 3 ether); + // (200_000 + 5_000_000 = 5_200_000 < 10_000_000) → 6 ether (occupancy doubled) + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(200_000, 5_000_000), 6 ether); + + // Scenario 3: Total committed gas above 50% threshold → congestion fee is added + // (100_000 + 10_000_000 = 10_100_000 > 10_000_000) + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(100_000, 10_000_000), 3.0911325 ether); + // (100_000 + 15_000_000 = 15_100_000 > 10_000_000, more congestion) + assertEq(IRegistryFacet(diamondAddr).estimateAutomationFeeWithCommittedOccupancy(100_000, 15_000_000), 11.72151126 ether); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/TaskMetadataLWGas.t.sol b/solidity/supra_contracts/test/TaskMetadataLWGas.t.sol new file mode 100644 index 0000000000..fbe3e1344b --- /dev/null +++ b/solidity/supra_contracts/test/TaskMetadataLWGas.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {IConfigFacet} from "../src/interfaces/IConfigFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {Deployment, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {ERC20SupraHandler} from "../src/ERC20SupraHandler.sol"; + +/// @notice Gas-comparison tests proving that task charging/lifecycle branches which do NOT +/// delete the task from storage (dropOrChargeTask's "stays active" branch, and cancelTask's +/// "already-active" branch) no longer scale with the size of a task's +/// payloadTx/predicate/auxData, now that they read TaskMetadataLW instead of copying the full +/// TaskMetadata struct from storage. +/// +/// Branches that DO delete the task (onCycleSuspend, handleTasksRemoval, stopTask, cancelTask's +/// PENDING branch, dropOrChargeTask's cancelled/expired branch) still legitimately cost more +/// gas for a bigger payload: `delete`-ing a storage struct always clears every field, including +/// the dynamic ones, regardless of what gets copied into memory beforehand. TaskMetadataLW +/// removes the redundant memory copy on those paths too, but can't make deletion itself +/// size-independent, so they are intentionally not covered by a gas-equality assertion here. +contract TaskMetadataLWGasTest is BaseDiamondTest { + /// @dev Gas tolerance between a "light" and a "heavy" task on the same code path. + /// Chosen well below the tens-of-thousands of gas that copying ~45KB of extra + /// payloadTx/predicate/auxData from storage would otherwise cost, so a regression + /// that reintroduces a full `TaskMetadata memory` copy would fail this assertion. + uint256 constant GAS_TOLERANCE = 5_000; + + function lightPayload() internal view returns (bytes memory) { + return createPayload(0, address(erc20SupraHandler), abi.encodeCall(ERC20SupraHandler.withdraw, 100)); + } + + /// @dev payloadTx is only length-checked (>= 4 bytes) and decoded at registration time in + /// this contract set — it is never executed on-chain here (execution happens off-chain via + /// the VM signer) — so padding the inner call data with junk bytes is a safe way to inflate + /// its size without affecting any validation. + function heavyPayload() internal view returns (bytes memory) { + bytes memory paddedCallData = abi.encodePacked(abi.encodeCall(ERC20SupraHandler.withdraw, 100), new bytes(5_000)); + return createPayload(0, address(erc20SupraHandler), paddedCallData); + } + + function heavyAuxData() internal pure returns (bytes[] memory auxData) { + auxData = new bytes[](20); + for (uint256 i = 0; i < auxData.length; i++) { + auxData[i] = new bytes(2_000); + } + } + + /// @dev Deploys an independent diamond sharing the same ERC20Supra token, so a "light" and a + /// "heavy" task can each be the sole/last task in their own cycle — avoiding the confound of + /// cycle-transition-finalization overhead (paid only by whichever task is processed last) + /// leaking into the payload-size comparison. + function deploySiblingDiamond() internal returns (address) { + vm.startPrank(admin); + Deployment memory dep = LibDiamondUtils.deploy(admin, address(erc20Supra), defaultParams); + // Raise the default input-size caps so this diamond can accept the intentionally + // oversized heavyPayload()/heavyAuxData() fixtures used to prove gas-independence. + IConfigFacet(dep.diamond).updateDataLengthCaps(type(uint16).max, type(uint16).max, type(uint16).max, type(uint16).max); + vm.stopPrank(); + return dep.diamond; + } + + /// @dev Registers task index 0 (always the first task on a freshly deployed diamond) as a UST. + function registerUstOn(address _diamond, bytes memory _payload, bytes[] memory _auxData) internal { + bytes memory predicate = createPredicate(_diamond); + + vm.startPrank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + erc20Supra.approve(_diamond, type(uint256).max); + + IRegistryFacet(_diamond).register( + _payload, + predicate, + uint64(block.timestamp + 2450), + uint128(100_000), + uint128(4 gwei), + uint128(60.1 ether), + 2, + _auxData + ); + vm.stopPrank(); + } + + /// @dev Warps to cycle end, finalizes it, and charges the sole task (index 0) on `_diamond`, + /// returning the gas used by that single `processTasks` call. + function chargeSoleTaskAndMeasureGas(address _diamond) internal returns (uint256 gasUsed) { + (uint64 indexBefore, uint64 startTimeBefore, uint64 durationBefore,) = ICoreFacet(_diamond).getCycleInfo(); + vm.warp(startTimeBefore + durationBefore); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(_diamond).monitorCycleEnd(); + + uint256[] memory tasks = new uint256[](1); + tasks[0] = 0; + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + uint256 gasBefore = gasleft(); + ICoreFacet(_diamond).processTasks(indexBefore + 1, tasks); + gasUsed = gasBefore - gasleft(); + } + + /// @dev Charging an active UST (dropOrChargeTask's "Active UST" branch, which never deletes + /// the task) must cost essentially the same gas whether the task carries a tiny or a ~45KB + /// payload. + function testDropOrChargeTaskGasIndependentOfPayloadSize() public { + bytes[] memory emptyAux; + registerUstOn(diamondAddr, lightPayload(), emptyAux); + + address heavyDiamond = deploySiblingDiamond(); + registerUstOn(heavyDiamond, heavyPayload(), heavyAuxData()); + + uint256 lightGas = chargeSoleTaskAndMeasureGas(diamondAddr); + uint256 heavyGas = chargeSoleTaskAndMeasureGas(heavyDiamond); + + assertApproxEqAbs(heavyGas, lightGas, GAS_TOLERANCE); + } + + /// @dev Cancelling an already-ACTIVE task (LibRegistry.cancelTask's non-PENDING branch, + /// which only overwrites `taskState` and never deletes the task) must cost the same gas + /// regardless of payload size. + function testCancelActiveTaskGasIndependentOfPayloadSize() public { + bytes[] memory emptyAux; + registerUstOn(diamondAddr, lightPayload(), emptyAux); + + address heavyDiamond = deploySiblingDiamond(); + registerUstOn(heavyDiamond, heavyPayload(), heavyAuxData()); + + chargeSoleTaskAndMeasureGas(diamondAddr); + chargeSoleTaskAndMeasureGas(heavyDiamond); + + uint64[] memory task = new uint64[](1); + task[0] = 0; + + vm.prank(alice); + uint256 gasBeforeLight = gasleft(); + IRegistryFacet(diamondAddr).cancelTasks(task); + uint256 lightGas = gasBeforeLight - gasleft(); + + vm.prank(alice); + uint256 gasBeforeHeavy = gasleft(); + IRegistryFacet(heavyDiamond).cancelTasks(task); + uint256 heavyGas = gasBeforeHeavy - gasleft(); + + assertApproxEqAbs(heavyGas, lightGas, GAS_TOLERANCE); + } +}