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..eda7dd2777 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,12 @@ +[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 +[submodule "solidity/supranova"] + path = solidity/supranova + url = ssh://git@github.com/Entropy-Foundation/supranova-contracts-private.git 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..725f19c656 100644 --- a/crates/context/interface/src/cfg.rs +++ b/crates/context/interface/src/cfg.rs @@ -4,6 +4,50 @@ 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 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 => { + 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) + } +} + /// Configuration for the EVM. #[auto_impl(&, &mut, Box, Arc)] pub trait Cfg { @@ -58,6 +102,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/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/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/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/handler/src/frame.rs b/crates/handler/src/frame.rs index 823ec568df..65702d7996 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 should_update_nonce = context.cfg().execution_mode().updates_nonce(); let return_error = |e| { Ok(ItemOrResult::Result(FrameResult::Create(CreateOutcome { result: InterpreterResult { @@ -296,16 +309,17 @@ 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; - let Some(new_nonce) = old_nonce.checked_add(1) else { - return return_error(InstructionResult::Return); - }; - caller_info.nonce = new_nonce; - context - .journal_mut() - .nonce_bump_journal_entry(inputs.caller); + if should_update_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); + }; + caller_info.nonce = new_nonce; + context + .journal_mut() + .nonce_bump_journal_entry(inputs.caller); + } // Create address let mut init_code_hash = None; diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index ad0c73ba96..1e4ed048ac 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,31 @@ pub trait Handler { validation::validate_env(evm.ctx()) } + /// Validates caller, to reject user transactions having caller address matching any of + /// the SUPRA reserved addresses. + #[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()) + { + // TODO create InvalidTransaction variant to report the error instead + Err(Self::Error::from_string(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_string(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. diff --git a/crates/handler/src/pre_execution.rs b/crates/handler/src/pre_execution.rs index 0c9bf87b9e..021fd58465 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(); @@ -166,10 +176,12 @@ pub fn validate_against_state_and_deduct_caller< caller_account.mark_touch(); 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()); diff --git a/crates/handler/src/precompile_provider.rs b/crates/handler/src/precompile_provider.rs index eb75bf6135..a2b90f5bcb 100644 --- a/crates/handler/src/precompile_provider.rs +++ b/crates/handler/src/precompile_provider.rs @@ -19,7 +19,7 @@ pub trait PrecompileProvider { /// Returned booling will determine if precompile addresses should be injected into the journal. fn set_spec(&mut self, spec: ::Spec) -> bool; - /// Run the precompile. + /// Run precompile. fn run( &mut self, context: &mut CTX, 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..8bee9ea7ce --- /dev/null +++ b/crates/supra-extension/Cargo.toml @@ -0,0 +1,53 @@ +[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 } +context = { workspace = true } +alloy-serde = {workspace = true, optional = true } +anyhow = { workspace = true } +foundry-compilers = { 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" + +[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"] diff --git a/crates/supra-extension/build.rs b/crates/supra-extension/build.rs new file mode 100644 index 0000000000..fb566f175d --- /dev/null +++ b/crates/supra-extension/build.rs @@ -0,0 +1,226 @@ +//! Prepares supra-extension by compiling smart-contracts and building rust bindings + +use anyhow::Result; +use bincode; +use foundry_compilers::utils; +use foundry_config::Config; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::env; +use std::path::Path; +use std::path::PathBuf; + +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"); +} + +#[derive(Serialize, Deserialize, Debug)] +struct CompileConfig { + /// Supra contracts relative path + supra_dapp_path: PathBuf, + /// Supra nova dapp relative path in repo + supra_nova_dapp_path: String, +} + +impl CompileConfig { + fn load() -> Result { + let path = Path::new(CURRENT_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)) + } + + fn supra_contracts_dapp_path(&self) -> PathBuf { + utils::canonicalize(Path::new(CURRENT_DIR).join(&self.supra_dapp_path)) + .expect("failed to canonicalize dapp path") + } + + fn supra_nova_dapp_path(&self) -> PathBuf { + utils::canonicalize(Path::new(CURRENT_DIR).join(&self.supra_nova_dapp_path)) + .expect("failed to canonicalize supranova dapp path") + } +} + +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(); + // Tell Cargo that if a source file changes, to rerun this build script. + project.rerun_if_sources_changed(); + println!("cargo:rerun-if-changed={}/compile_config.toml", CURRENT_DIR); + + let artifacts_dir = project.paths.artifacts.clone(); + println!( + "cargo:rustc-env=COMPILED_CONTRACTS_DIR={}", + artifacts_dir.display() + ); + + Ok(artifacts_dir) +} + +fn load_supra_contracts_bytecode( + artifacts_path: &Path, + bytecodes: &mut BTreeMap>, +) -> Result<()> { + // Contract names to load + let contract_names = [ + "MultiSignatureWallet", + "MultisigBeacon", + "BeaconProxy", + "ERC20Supra", + "ERC20SupraHandler", + "BlockMeta", + "ERC1967Proxy", + "DiamondCutFacet", + "Diamond", + "DiamondLoupeFacet", + "OwnershipFacet", + "ConfigFacet", + "RegistryFacet", + "CoreFacet", + "DiamondInit", + ]; + load_contracts_bytecode(&contract_names, artifacts_path, bytecodes) +} + +fn load_supra_nova_contracts_bytecode( + artifacts_path: &Path, + bytecodes: &mut BTreeMap>, +) -> Result<()> { + // Contract names to load + let contract_names = [ + "WrappedToken", // Impl + "WrappedTokenFactory", // Beacon + "WrappedTokenFactoryProxy", // Beacon Proxy + "TokenVault", + "TokenVaultProxy", + "Hypernova", + "HypernovaProxy", + "FeeOperator", + "FeeOperatorProxy", + "TokenBridge", + "TokenBridgeProxy", + ]; + load_contracts_bytecode(&contract_names, artifacts_path, bytecodes) +} + +fn load_contracts_bytecode( + contract_names: &[&'static str], + artifacts_path: &Path, + bytecodes: &mut BTreeMap>, +) -> Result<()> { + // Load each contract's bytecode + 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); + if inserted.is_some() { + return Err(anyhow::anyhow!( + "Duplicate contract name: {contract_name} in {artifacts_path:?} path" + )); + } + } + Ok(()) +} + +fn dump_bytecodes(bytecodes: BTreeMap>, bin_file_name: &str) -> Result<()> { + // Dump the combined contract bytecodes to be loaded at compile to by generator. + let out_dir = 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 serializationA"), + ) + .expect("Failed to write bytecodes to file"); + + println!("cargo:rustc-env=CONTRACTS_DUMPED=1"); + Ok(()) +} + +fn main() { + rebuild_rust_bindings(); + + let config = CompileConfig::load().expect("Config should always be valid"); + let supra_contracts_artifacts = compile_contracts(&config.supra_contracts_dapp_path()) + .expect("Successful supra contracts compilation"); + let supra_nova_artifacts = compile_contracts(&config.supra_nova_dapp_path()) + .expect("Successful supra nova contracts compilation"); + let mut contracts_bytecode = BTreeMap::new(); + load_supra_contracts_bytecode(&supra_contracts_artifacts, &mut contracts_bytecode) + .expect("Supra contracts loaded successfully"); + load_supra_nova_contracts_bytecode(&supra_nova_artifacts, &mut contracts_bytecode) + .expect("Supra nova contracts loaded successfully"); + dump_bytecodes(contracts_bytecode, "supra_contracts_bytecode") + .expect("Bytecodes dumped successfully"); +} diff --git a/crates/supra-extension/compile_config.toml b/crates/supra-extension/compile_config.toml new file mode 100644 index 0000000000..0611cc5aaa --- /dev/null +++ b/crates/supra-extension/compile_config.toml @@ -0,0 +1,4 @@ +supra_dapp_path = "../../solidity/supra_contracts/" +supra_nova_dapp_path = "../../solidity/supranova/hypernova/evm" + + diff --git a/crates/supra-extension/src/contracts/configs.rs b/crates/supra-extension/src/contracts/configs.rs new file mode 100644 index 0000000000..5b3b2068ef --- /dev/null +++ b/crates/supra-extension/src/contracts/configs.rs @@ -0,0 +1,154 @@ +//! Configurations to generate genesis transactions + +use serde::{Deserialize, Serialize}; +use primitives::{address, Address}; + +/// 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, + /// 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 Default for AutomationRegistryConfigV1 { + fn default() -> Self { + Self { + // 7 days + task_duration_cap_secs: 604800, + registry_max_gas_cap: 8_000_000, + // 0.004 SUPRA normalized based on the supra denominator between move and evm currency + automation_base_fee_wei_per_sec: 1_714_530_600_000, + // 0.05 SUPRA normalized based on the supra denominator between move and evm currency + flat_registration_fee_wei: 21_431_633_000_000, + congestion_threshold_percentage: 50, + // 0.004 SUPRA normalized based on the supra denominator between move and evm currency + congestion_base_fee_wei_per_sec: 1_714_530_600_000, + congestion_exponent: 6, + task_capacity: 400, + cycle_duration_secs: 600, + // ~1 month + sys_task_duration_cap_secs: 2626560, + sys_registry_max_gas_cap: 2_000_000, + sys_task_capacity: 100, + enable_automation_feature: true, + } + } +} + +/// 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) + } +} + +impl From for AutomationRegistryConfig { + fn from(config: AutomationRegistryConfigV1) -> Self { + Self::V1(config) + } +} + +/// Configuration to generate supra-nova contracts for genesis +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SupraNovaConfig { + /// Dora storage addressed utilized by fee operator. + pub dora_storage_address: Address, + /// WETH9 address utilized by token vault and token bridge. + pub weth9_address: Address, + /// Hypernova message index. + pub hypernova_msg_id: u64, + /// USDT pair index in Dora. + pub supra_usdt_pair_idx: u64, + /// Maximum stale oracle price limit. + pub max_stale_oracle_price_limit: u64, +} + +impl Default for SupraNovaConfig { + fn default() -> Self { + Self { + dora_storage_address: Self::DORA_STORAGE_TESTNET, + weth9_address: Self::WETH9_TESTNET, + hypernova_msg_id: 0, + supra_usdt_pair_idx: Self::SUPRA_USDT_PAIR_INDEX, + max_stale_oracle_price_limit: Self::MAX_STALE_ORACLE_PRICE_LIMIT, + } + } +} + + +impl SupraNovaConfig { + pub(crate) const WRAPPED_TOKEN_IMPL_SALT: &'static str = "supranova.WrappedToken.v1"; + pub(crate) const WRAPPED_TOKEN_FACTORY_IMPL_SALT:&'static str = "supranova.WrappedTokenFactory.v1"; + pub(crate) const WRAPPED_TOKEN_FACTORY_PROXY_SALT: &'static str = "supranova.WrappedTokenFactoryProxy.v1"; + + pub(crate) const HYPER_NOVA_IMPL_SALT: &'static str = "supranova.Hypernova.v2"; + pub(crate) const HYPER_NOVA_PROXY_SALT: &'static str = "supranova.HypernovaProxy.v2"; + + pub(crate) const FEE_OPERATOR_IMPL_SALT: &'static str = "supranova.FeeOperator.v2"; + pub(crate) const FEE_OPERATOR_PROXY_SALT: &'static str = "supranova.FeeOperatorProxy.v2"; + + pub(crate) const DORA_STORAGE_TESTNET: Address = address!("0x131918bC49Bb7de74aC7e19d61A01544242dAA80"); + pub(crate) const SUPRA_USDT_PAIR_INDEX: u64 = 500; + pub(crate) const MAX_STALE_ORACLE_PRICE_LIMIT:u64 = 86400; + + pub(crate) const TOKEN_BRIDGE_IMPL_SALT: &'static str = "supranova.TokenBridge.v2"; + pub(crate) const TOKEN_BRIDGE_PROXY_SALT: &'static str = "supranova.TokenBridgeProxy.v2"; + + pub(crate) const WETH9_TESTNET: Address = address!("0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"); + + + pub(crate) const TOKEN_VAULT_IMPL_SALT: &'static str = "supranova.TokenVault.v2"; + pub(crate) const TOKEN_VAULT_PROXY_SALT: &'static str = "supranova.TokenVaultProxy.v2"; + +} + +/// 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, + /// Initial native tokens to be minted to ERC20Supra handler contract + pub initial_native_token: u128, + #[serde(skip_serializing_if = "Option::is_none")] + /// Indicates whether the genesis transactions are generated for localnet. + pub supra_nova_config: Option, +} diff --git a/crates/supra-extension/src/contracts/generator.rs b/crates/supra-extension/src/contracts/generator.rs new file mode 100644 index 0000000000..9784eeb60d --- /dev/null +++ b/crates/supra-extension/src/contracts/generator.rs @@ -0,0 +1,1186 @@ +//! Encloses transaction data generation logic based on the genesis contracts + +use crate::contracts::configs::{ + AutomationRegistryConfig, GenesisTransactionGeneratorConfig, SupraNovaConfig, +}; +use crate::contracts::supra_nova_contracts::{ + FeeOperator, FeeOperatorProxy, Hypernova, HypernovaProxy, TokenBridge, TokenBridgeProxy, + TokenVault, TokenVaultProxy, WrappedTokenFactory, WrappedTokenFactoryProxy, FEE_OPERATOR, + FEE_OPERATOR_PROXY, HYPERNOVA, HYPERNOVA_PROXY, TOKEN_BRIDGE, TOKEN_BRIDGE_PROXY, TOKEN_VAULT, + TOKEN_VAULT_PROXY, WRAPPED_TOKEN, WRAPPED_TOKEN_FACTORY, WRAPPED_TOKEN_FACTORY_PROXY, +}; +use crate::contracts::transaction::{ + GenesisTransaction, GenesisTransactionTags, CREATE2_FACTORY_ADDRESS, CREATE2_FACTORY_CODE, + CREATE2_FACTORY_OWNER, +}; +use alloy::primitives::Address; +use alloy_sol_types::{sol, SolCall, SolConstructor}; +use anyhow::{anyhow, Result}; +use bincode::config; +use once_cell::sync::Lazy; +use primitives::supra_constants::VM_SIGNER; +use primitives::{Bytes, TxKind, 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")); + +const 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); + } +} + +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; + 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)] +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 } + } + + /// Generates Create2Factory contract deployment transaction. + fn generate_create2_factory_transaction() -> GenesisTransaction { + GenesisTransaction::new( + CREATE2_FACTORY_OWNER, + 0, + 0, + CREATE2_FACTORY_CODE.to_owned(), + TxKind::Create, + CREATE2_FACTORY_ADDRESS, + ) + } + + /// Prepares genesis transactions based on the input configuration. + pub fn prepare_genesis_transactions( + &mut self, + config: GenesisTransactionGeneratorConfig, + ) -> Result> { + let GenesisTransactionGeneratorConfig { + foundation_owners, + foundation_threshold, + full_set, + automation_config, + initial_native_token, + supra_nova_config, + } = config; + // First Create2 Factory contract deployment, which will allow later to utilize create2 API + // if required during genesis + let mut genesis_transactions = BTreeMap::from([( + GenesisTransactionTags::Create2Factory, + Self::generate_create2_factory_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(); + + // Erc20 Supra contracts + let erc20_contracts = + self.setup_erc20_contracts(multisig_address, initial_native_token)?; + let erc20supra_address = *erc20_contracts + .get(&GenesisTransactionTags::Erc20Supra) + .expect("Erc20Supra deployment transaction exists") + .deploy_address(); + genesis_transactions.extend(erc20_contracts); + + // BlockMetadata contract + genesis_transactions.extend(self.setup_block_metadata(multisig_address)?.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(), + ); + } + + // Supra Nova/Bridge contracts + if let Some(nova_conig) = supra_nova_config { + genesis_transactions + .extend(self.setup_supra_nova_contracts(nova_conig, multisig_address)?); + } + }; + + 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, + initial_native_tokens: u128, + ) -> 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(); + 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, initial_native_tokens)?; + let gen_erc20_handler_address = *erc20_handler_txn + .get(&GenesisTransactionTags::Erc20SupraHandler) + .expect("Erc20Supra should be deployed") + .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, + initial_native_tokens: u128, + ) -> 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::new( + self.address, + self.nonce, + initial_native_tokens, + proxy_txn_data, + TxKind::Create, + 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, + ) -> 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) + // ------------------------------------------------------------------------- + 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, + } + .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, + 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 setup_supra_nova_contracts( + &mut self, + config: SupraNovaConfig, + owner: Address, + ) -> Result> { + // First setup all independent contracts + // 1. Wrapped token contracts + // 2. Hypernova contracts + // 3. Token Vault contracts + let wrapped_token_contracts = self.setup_wrapped_token_contracts(owner)?; + let hyper_nova_contracts = self.setup_hyper_nova_contracts(owner, &config)?; + let token_vault_contracts = self.setup_token_vault_contracts(owner, &config)?; + + // 4. Setup FeeOperator contracts which depends on hypernova deployment + let hyper_nova = *hyper_nova_contracts + .get(&GenesisTransactionTags::HypernovaProxy) + .expect("Hypernova contract should be deployed") + .deploy_address(); + + let fee_operator_contracts = + self.setup_fee_operator_contracts(owner, hyper_nova, &config)?; + + // 5. Setup Token Bridge contracts which depends on all above + let token_vault = *token_vault_contracts + .get(&GenesisTransactionTags::TokenVaultProxy) + .expect("TokenVault contract should be deployed") + .deploy_address(); + + let fee_operator = *fee_operator_contracts + .get(&GenesisTransactionTags::FeeOperatorProxy) + .expect("FeeOperator contract should be deployed") + .deploy_address(); + + let wrapped_token = *wrapped_token_contracts + .get(&GenesisTransactionTags::WrappedTokenFactoryProxy) + .expect("WrappedToken contract should be deployed") + .deploy_address(); + + let token_bridge_contracts = self.setup_token_bridge_contracts( + owner, + hyper_nova, + token_vault, + fee_operator, + wrapped_token, + &config, + )?; + + let mut contract_txns = wrapped_token_contracts; + contract_txns.extend(hyper_nova_contracts); + contract_txns.extend(token_vault_contracts); + contract_txns.extend(fee_operator_contracts); + contract_txns.extend(token_bridge_contracts); + + Ok(contract_txns) + } + + fn setup_wrapped_token_contracts( + &mut self, + owner: Address, + ) -> Result> { + let wrapped_token_init_data = Self::load_contract_bytecode(WRAPPED_TOKEN)?; + let wrapped_token_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::WRAPPED_TOKEN_IMPL_SALT, + wrapped_token_init_data, + self.nonce, + ); + self.nonce += 1; + + let wrapped_token_fct_init_data = Self::load_contract_bytecode(WRAPPED_TOKEN_FACTORY)?; + let wrapped_token_fct_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::WRAPPED_TOKEN_FACTORY_IMPL_SALT, + wrapped_token_fct_init_data, + self.nonce, + ); + self.nonce += 1; + + let wrapped_token_fct_init_call = WrappedTokenFactory::initializeCall { + owner, + token_impl: *wrapped_token_txn.deploy_address(), + } + .abi_encode(); + + let wrapped_token_proxy_init_data = + Self::load_contract_bytecode(WRAPPED_TOKEN_FACTORY_PROXY)?; + let wrapped_token_proxy_cnstr_data = WrappedTokenFactoryProxy::constructorCall { + factory_impl: *wrapped_token_fct_txn.deploy_address(), + init_data: wrapped_token_fct_init_call.into(), + } + .abi_encode(); + let wrapped_token_proxy_txn_data = [ + wrapped_token_proxy_init_data, + wrapped_token_proxy_cnstr_data, + ] + .concat(); + let wrapped_token_proxy_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::WRAPPED_TOKEN_FACTORY_PROXY_SALT, + wrapped_token_proxy_txn_data, + self.nonce, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::WrappedToken, wrapped_token_txn), + ( + GenesisTransactionTags::WrappedTokenFactory, + wrapped_token_fct_txn, + ), + ( + GenesisTransactionTags::WrappedTokenFactoryProxy, + wrapped_token_proxy_txn, + ), + ])) + } + + fn setup_hyper_nova_contracts( + &mut self, + owner: Address, + config: &SupraNovaConfig, + ) -> Result> { + let hyper_nova_init_data = Self::load_contract_bytecode(HYPERNOVA)?; + let hyper_nova_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::HYPER_NOVA_IMPL_SALT, + hyper_nova_init_data, + self.nonce, + ); + self.nonce += 1; + + let hyper_nova_init_call_data = Hypernova::initializeCall { + owner, + msgId: U256::from(config.hypernova_msg_id), + } + .abi_encode(); + + let hyper_nova_proxy_init_data = Self::load_contract_bytecode(HYPERNOVA_PROXY)?; + let hyper_nova_proxy_cnstr_data = HypernovaProxy::constructorCall { + hypernova_impl: *hyper_nova_txn.deploy_address(), + init_data: hyper_nova_init_call_data.into(), + } + .abi_encode(); + let hyper_nova_proxy_txn_data = + [hyper_nova_proxy_init_data, hyper_nova_proxy_cnstr_data].concat(); + let hyper_nova_proxy_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::HYPER_NOVA_PROXY_SALT, + hyper_nova_proxy_txn_data, + self.nonce, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::Hypernova, hyper_nova_txn), + (GenesisTransactionTags::HypernovaProxy, hyper_nova_proxy_txn), + ])) + } + + fn setup_token_vault_contracts( + &mut self, + owner: Address, + config: &SupraNovaConfig, + ) -> Result> { + let token_vault_init_data = Self::load_contract_bytecode(TOKEN_VAULT)?; + let token_vault_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::TOKEN_VAULT_IMPL_SALT, + token_vault_init_data, + self.nonce, + ); + self.nonce += 1; + + let token_vault_init_call_data = TokenVault::initializeCall { + owner, + nativeToken: config.weth9_address, + brigde: owner, + } + .abi_encode(); + + let token_vault_proxy_init_data = Self::load_contract_bytecode(TOKEN_VAULT_PROXY)?; + let token_vault_proxy_cnstr_data = TokenVaultProxy::constructorCall { + token_vault_impl: *token_vault_txn.deploy_address(), + init_data: token_vault_init_call_data.into(), + } + .abi_encode(); + let token_vault_proxy_txn_data = + [token_vault_proxy_init_data, token_vault_proxy_cnstr_data].concat(); + let token_vault_proxy_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::TOKEN_VAULT_PROXY_SALT, + token_vault_proxy_txn_data, + self.nonce, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::TokenVault, token_vault_txn), + ( + GenesisTransactionTags::TokenVaultProxy, + token_vault_proxy_txn, + ), + ])) + } + + fn setup_fee_operator_contracts( + &mut self, + owner: Address, + hyper_nova: Address, + config: &SupraNovaConfig, + ) -> Result> { + let fee_operator_init_data = Self::load_contract_bytecode(FEE_OPERATOR)?; + let fee_operator_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::FEE_OPERATOR_IMPL_SALT, + fee_operator_init_data, + self.nonce, + ); + self.nonce += 1; + + let fee_operator_init_call_data = FeeOperator::initializeCall { + owner, + hypernova: hyper_nova, + sValueFeed: config.dora_storage_address, + supraUsdtPairIndex: U256::from(config.supra_usdt_pair_idx), + maxStaleOraclePriceLimit: U256::from(config.max_stale_oracle_price_limit), + } + .abi_encode(); + + let fee_operator_proxy_init_data = Self::load_contract_bytecode(FEE_OPERATOR_PROXY)?; + let fee_operator_proxy_cnstr_data = FeeOperatorProxy::constructorCall { + fee_operator_impl: *fee_operator_txn.deploy_address(), + init_data: fee_operator_init_call_data.into(), + } + .abi_encode(); + let fee_operator_proxy_txn_data = + [fee_operator_proxy_init_data, fee_operator_proxy_cnstr_data].concat(); + let fee_operator_proxy_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::FEE_OPERATOR_PROXY_SALT, + fee_operator_proxy_txn_data, + self.nonce, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::FeeOperator, fee_operator_txn), + ( + GenesisTransactionTags::FeeOperatorProxy, + fee_operator_proxy_txn, + ), + ])) + } + + fn setup_token_bridge_contracts( + &mut self, + owner: Address, + hyper_nova: Address, + fee_operator_address: Address, + token_vault_address: Address, + wrapped_token_proxy_address: Address, + config: &SupraNovaConfig, + ) -> Result> { + let token_bridge_init_data = Self::load_contract_bytecode(TOKEN_BRIDGE)?; + let token_bridge_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::TOKEN_BRIDGE_IMPL_SALT, + token_bridge_init_data, + self.nonce, + ); + self.nonce += 1; + + let token_bridge_init_call_data = TokenBridge::initializeCall { + owner, + nativeToken: config.weth9_address, + hypernova: hyper_nova, + feeOperator: fee_operator_address, + vault: token_vault_address, + wrappedTokenFactory: wrapped_token_proxy_address, + } + .abi_encode(); + + let token_bridge_proxy_init_data = Self::load_contract_bytecode(TOKEN_BRIDGE_PROXY)?; + let token_bridge_proxy_cnstr_data = TokenBridgeProxy::constructorCall { + token_bridge_impl: *token_bridge_txn.deploy_address(), + init_data: token_bridge_init_call_data.into(), + } + .abi_encode(); + let token_bridge_proxy_txn_data = + [token_bridge_proxy_init_data, token_bridge_proxy_cnstr_data].concat(); + let token_bridge_proxy_txn = GenesisTransaction::create2( + self.address, + SupraNovaConfig::TOKEN_BRIDGE_PROXY_SALT, + token_bridge_proxy_txn_data, + self.nonce, + ); + self.nonce += 1; + + Ok(BTreeMap::from([ + (GenesisTransactionTags::TokenBridge, token_bridge_txn), + ( + GenesisTransactionTags::TokenBridgeProxy, + token_bridge_proxy_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; + + #[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 initial_native_token = 1000; + let mut config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: false, + automation_config: None, + initial_native_token, + supra_nova_config: None, + }; + let result = generator + .prepare_genesis_transactions(config.clone()) + .unwrap(); + assert_eq!(result.len(), 4); + assert!(result.contains_key(&GenesisTransactionTags::Create2Factory)); + assert!(result.contains_key(&GenesisTransactionTags::MultisigWalletImpl)); + assert!(result.contains_key(&GenesisTransactionTags::MultisigBeacon)); + assert!(result.contains_key(&GenesisTransactionTags::FoundationWallet)); + + // 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(), &initial_native_token); + + // 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 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: 1000, + ..Default::default() + }; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: true, + automation_config: Some(custom_config.into()), + initial_native_token: 1000, + supra_nova_config: None, + }; + 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:#?}"); + } + + #[test] + fn check_supra_nova_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: 1000, + ..Default::default() + }; + let config = GenesisTransactionGeneratorConfig { + foundation_owners: owners, + foundation_threshold: 2, + full_set: true, + automation_config: Some(custom_config.into()), + initial_native_token: 1000, + supra_nova_config: Some(SupraNovaConfig::default()), + }; + let result = generator + .prepare_genesis_transactions(config) + .expect("Successful txn generation"); + + // Verify all automation contracts are deployed + assert!(result.contains_key(&GenesisTransactionTags::WrappedToken)); + assert!(result.contains_key(&GenesisTransactionTags::WrappedTokenFactory)); + assert!(result.contains_key(&GenesisTransactionTags::WrappedTokenFactoryProxy)); + assert!(result.contains_key(&GenesisTransactionTags::Hypernova)); + assert!(result.contains_key(&GenesisTransactionTags::HypernovaProxy)); + assert!(result.contains_key(&GenesisTransactionTags::FeeOperator)); + assert!(result.contains_key(&GenesisTransactionTags::FeeOperatorProxy)); + assert!(result.contains_key(&GenesisTransactionTags::TokenVault)); + assert!(result.contains_key(&GenesisTransactionTags::TokenVaultProxy)); + assert!(result.contains_key(&GenesisTransactionTags::TokenBridge)); + assert!(result.contains_key(&GenesisTransactionTags::TokenBridgeProxy)); + println!("{result:#?}"); + } +} diff --git a/crates/supra-extension/src/contracts/mod.rs b/crates/supra-extension/src/contracts/mod.rs new file mode 100644 index 0000000000..4d0b2a65e3 --- /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 configs; +pub mod generator; +pub mod transaction; +pub(crate) mod supra_nova_contracts; diff --git a/crates/supra-extension/src/contracts/supra_nova_contracts.rs b/crates/supra-extension/src/contracts/supra_nova_contracts.rs new file mode 100644 index 0000000000..6223d76812 --- /dev/null +++ b/crates/supra-extension/src/contracts/supra_nova_contracts.rs @@ -0,0 +1,67 @@ +use alloy_sol_types::sol; + +pub(crate) const WRAPPED_TOKEN: &str = "WrappedToken"; +pub(crate) const WRAPPED_TOKEN_FACTORY: &str = "WrappedTokenFactory"; +pub(crate) const WRAPPED_TOKEN_FACTORY_PROXY: &str = "WrappedTokenFactoryProxy"; +pub(crate) const HYPERNOVA: &str = "Hypernova"; +pub(crate) const HYPERNOVA_PROXY: &str = "HypernovaProxy"; +pub(crate) const TOKEN_VAULT: &str = "TokenVault"; +pub(crate) const TOKEN_VAULT_PROXY: &str = "TokenVaultProxy"; +pub(crate) const FEE_OPERATOR: &str = "FeeOperator"; +pub(crate) const FEE_OPERATOR_PROXY: &str = "FeeOperatorProxy"; +pub(crate) const TOKEN_BRIDGE: &str = "TokenBridge"; +pub(crate) const TOKEN_BRIDGE_PROXY: &str = "TokenBridgeProxy"; + +sol! { + contract WrappedTokenFactory { + function initialize(address owner, address token_impl); + } + contract WrappedTokenFactoryProxy { + constructor(address factory_impl, bytes init_data); + } + + contract Hypernova { + function initialize(address owner, uint256 msgId); + } + + contract HypernovaProxy { + constructor(address hypernova_impl, bytes init_data); + } + + contract TokenVault { + function initialize(address owner, address nativeToken, address brigde); + } + + contract TokenVaultProxy { + constructor(address token_vault_impl, bytes init_data); + } + + contract FeeOperator { + function initialize( + address owner, + address hypernova, + address sValueFeed, + uint256 supraUsdtPairIndex, + uint256 maxStaleOraclePriceLimit); + } + + contract FeeOperatorProxy { + constructor(address fee_operator_impl, bytes init_data); + } + + contract TokenBridge { + function initialize( + address owner, + address nativeToken, + address hypernova, + address feeOperator, + address vault, + address wrappedTokenFactory + ); + } + + contract TokenBridgeProxy { + constructor(address token_bridge_impl, bytes init_data); + } + +} diff --git a/crates/supra-extension/src/contracts/transaction.rs b/crates/supra-extension/src/contracts/transaction.rs new file mode 100644 index 0000000000..e23ceef690 --- /dev/null +++ b/crates/supra-extension/src/contracts/transaction.rs @@ -0,0 +1,143 @@ +//! Encloses data representing genesis contracts. + +use derive_getters::{Dissolve, Getters}; +use derive_more::Constructor; +use primitives::{keccak256, Address, TxKind, address, hex}; +use std::fmt::{Debug, Display}; +use serde::{Serialize, Deserialize}; +use serde_with::hex::Hex ; +use serde_with::serde_as; + + +/// 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] = &hex!( + "604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" +); + +/// 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: Address, +} + +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, + 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), + deploy_address, + ) + } + +} + +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() + } +} + +/// 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 { + Create2Factory = 0, + // Main system and foundation contracts + MultisigWalletImpl = 1, + MultisigBeacon = 2, + FoundationWallet = 3, + Erc20SupraImpl = 4, + Erc20Supra = 5, + Erc20SupraHandlerImpl = 6, + Erc20SupraHandler = 7, + BlockMetadataImpl = 8, + BlockMetadata = 9, + + // Automation registry contracts + DiamondCutFacet = 10, + DiamondLoupeFacet = 11, + OwnershipFacet = 12, + ConfigFacet = 13, + RegistryFacet = 14, + CoreFacet = 15, + DiamondInit = 16, + Diamond = 17, + + // Supra Nova contracts + WrappedToken = 18, // Impl + WrappedTokenFactory = 19, // Beacon + WrappedTokenFactoryProxy = 20, // Beacon Proxy + Hypernova = 21, + HypernovaProxy = 22, + TokenVault = 23, + TokenVaultProxy = 24, + FeeOperator = 25, + FeeOperatorProxy = 26, + TokenBridge = 27, + TokenBridgeProxy = 28, +} + +impl Display for GenesisTransactionTags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/supra-extension/src/errors.rs b/crates/supra-extension/src/errors.rs new file mode 100644 index 0000000000..2132c2bd94 --- /dev/null +++ b/crates/supra-extension/src/errors.rs @@ -0,0 +1,52 @@ +//! 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 {payload} payload: {error}")] + PayloadDecode { + /// Error description + error: alloy_sol_types::Error, + /// Payload description for which error has been identified + payload: String + }, + + /// 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..9c8c245dc7 --- /dev/null +++ b/crates/supra-extension/src/lib.rs @@ -0,0 +1,13 @@ +//! # revm-supra-extension +//! Supra extensions of the transactions to support automation feature and block based checks + +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..f5d9dc2291 --- /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 isAutomationEnabled() 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": "isAutomationEnabled", + "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 `isAutomationEnabled()` and selector `0xe48e0e98`. +```solidity +function isAutomationEnabled() external view returns (bool); +```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isAutomationEnabledCall; + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`isAutomationEnabled()`](isAutomationEnabledCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct isAutomationEnabledReturn { + #[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: isAutomationEnabledCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for isAutomationEnabledCall { + 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: isAutomationEnabledReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> + for isAutomationEnabledReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for isAutomationEnabledCall { + 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 = "isAutomationEnabled()"; + const SELECTOR: [u8; 4] = [228u8, 142u8, 14u8, 152u8]; + #[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: isAutomationEnabledReturn = 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: isAutomationEnabledReturn = 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)] + isAutomationEnabled(isAutomationEnabledCall), + #[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]] = &[ + [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], + [228u8, 142u8, 14u8, 152u8], + [236u8, 130u8, 180u8, 41u8], + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::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!(isAutomationEnabled), + ::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::isAutomationEnabled(_) => { + ::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 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 isAutomationEnabled( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw( + data, + ) + .map(SupraContractsBindingsCalls::isAutomationEnabled) + } + isAutomationEnabled + }, + { + 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 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 isAutomationEnabled( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(SupraContractsBindingsCalls::isAutomationEnabled) + } + isAutomationEnabled + }, + { + 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::isAutomationEnabled(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::isAutomationEnabled(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 [`isAutomationEnabled`] function. + pub fn isAutomationEnabled( + &self, + ) -> alloy_contract::SolCallBuilder<&P, isAutomationEnabledCall, N> { + self.call_builder(&isAutomationEnabledCall) + } + ///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..a2c372e442 --- /dev/null +++ b/crates/supra-extension/src/transactions/automated_transaction.rs @@ -0,0 +1,1253 @@ +//! 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 { + if value.is_empty() { + return Ok(Self::default()); + } + 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::PayloadDecode { + error: e, + payload: "predicate".to_owned(), + } + })?; + 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), +} + +#[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(value).map_err(|e| { + SupraExtensionError::PayloadDecode { + error: e, + payload: "task action".to_owned(), + } + })?; + 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: TaskPredicate) -> 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 + .map(AutomationTaskPredicate::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 = TaskPredicate::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 alloy::primitives::{address, b256, Address, B256, Bytes, U256}; + use alloy_consensus::transaction::Transaction; + use alloy_sol_types::SolType; + use crate::{errors::SupraExtensionError, TaskMetadata}; + use crate::transactions::automated_transaction::{ExpandedPayloadTy, TaskPredicate}; + use alloy::hex; + + 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(&( + 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_default() { + let p = TaskPredicate::try_from([].as_slice()).unwrap(); + assert_eq!(p, TaskPredicate::default()); + assert_eq!(p.address, Address::ZERO); + assert!(p.input.is_empty()); + } + + #[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::PayloadDecode { .. })); + } + + // ── 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(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(); + assert_eq!(predicate.address, pred_addr); + assert_eq!(predicate.input, Bytes::from(b"pred_input")); + } + + #[test] + fn task_metadata_empty_predicate_produces_default_predicate_in_builder() { + // Empty predicate bytes → TaskPredicate::default() (address=ZERO, input=empty) + // 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_eq!(*builder.predicate(), Some(TaskPredicate::default())); + } + + #[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::PayloadDecode { .. })); + } + + #[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!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b182f1488e8efeb2eb298155ed5bd7ff8a14042000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000242e1a7d4d0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000022220000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"); + let (value, to, input, access_list) = ExpandedPayloadTy::abi_decode(&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 payload = TaskPredicate::try_from(encoded.as_slice()).unwrap(); + println!("to: {:?}", payload.address); + println!("input: {:?}", payload.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..d59967d810 --- /dev/null +++ b/crates/supra-extension/src/transactions/automation_record.rs @@ -0,0 +1,868 @@ +//! 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::PayloadDecode { error: e, payload: "AutomationRecordAction::Remove".to_string() }) + .map(AutomationRecordAction::Remove) + } else if processTasksCall::SELECTOR.as_slice().eq(selector) { + processTasksCall::abi_decode(&self.input) + .map_err(|e| SupraExtensionError::PayloadDecode { error: e, payload: "AutomationRecordAction::Process".to_string() }) + .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 +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumKind)] +#[enum_kind(AutomationRecordActionTag)] +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)] +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 + } + + 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..9a1ae13cf4 --- /dev/null +++ b/crates/supra-extension/src/transactions/block_metadata.rs @@ -0,0 +1,329 @@ +//! 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::errors::SupraExtensionError; +use crate::blockPrologueCall; +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; + +/// EVM system transaction generated based on the block sent for execution. +/// Will trigger `BlockMeta::block_prologue` supra-evm SC API execution to meat +/// 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, +} + +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 { + TX_GAS_LIMIT_CAP + } + + #[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, +} + +#[allow(missing_docs)] +impl BlockMetadataBuilder { + pub fn new(to: Address) -> Self { + Self { + to, + height: None, + block_hash: None, + timestamp: None, + chain_id: None, + } + } + 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 build(self) -> Result { + let Self { + to, + height, + block_hash, + timestamp, + chain_id, + } = 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); + + Ok(BlockMetadata { + chain_id, + sender: VM_SIGNER, + height, + block_hash, + timestamp, + to, + input: Self::get_block_prologue(), + }) + } + + fn get_block_prologue() -> Bytes { + Bytes::from(blockPrologueCall.abi_encode()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{address, b256, Address, B256, U256}; + use alloy_consensus::transaction::Transaction; + use alloy_eips::eip2718::Typed2718; + use crate::errors::SupraExtensionError; + use primitives::eip7825::TX_GAS_LIMIT_CAP; + 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; + + fn full_builder() -> BlockMetadataBuilder { + BlockMetadataBuilder::new(REGISTRY) + .height(HEIGHT) + .block_hash(BLOCK_HASH) + .timestamp(U256::from(TIMESTAMP)) + .chain_id(CHAIN_ID) + } + + // ── 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); + } + + #[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")); + } + + // ── 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(), TX_GAS_LIMIT_CAP); + 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()); + } +} 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/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..da0cba6662 --- /dev/null +++ b/solidity/supra_contracts/foundry.toml @@ -0,0 +1,18 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +via_ir = true +optimizer = true +evm_version = "prague" +# 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/DeployBlockMeta.s.sol b/solidity/supra_contracts/script/DeployBlockMeta.s.sol new file mode 100644 index 0000000000..b1fac81e42 --- /dev/null +++ b/solidity/supra_contracts/script/DeployBlockMeta.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + console.log("BlockMeta proxy deployed at: ", address(proxy)); + + // Register the selector + BlockMeta(address(proxy)).register(automationController, selector); + + 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..e50edb70bb --- /dev/null +++ b/solidity/supra_contracts/script/DeployDiamond.s.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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")), + 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..9880a81a07 --- /dev/null +++ b/solidity/supra_contracts/script/DeployERC20Supra.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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..fbcb0ae72c --- /dev/null +++ b/solidity/supra_contracts/script/DeployERC20SupraHandler.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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..823ad65911 --- /dev/null +++ b/solidity/supra_contracts/script/DeployMultisig.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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..2fc234223e --- /dev/null +++ b/solidity/supra_contracts/script/GovActions.s.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 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()")); + 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)); + 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..19e360b2b2 --- /dev/null +++ b/solidity/supra_contracts/script/MintErc20Supra.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 conf_all = erc20Supra.allowance(msg.sender, authority); + + console.log("Sender: ", msg.sender, conf_all, 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..fd1d7314d0 --- /dev/null +++ b/solidity/supra_contracts/script/RegisterAutomationTask.s.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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..ea74d27a90 --- /dev/null +++ b/solidity/supra_contracts/script/TxHashPrecompile.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + + +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/src/BlockMeta.sol b/solidity/supra_contracts/src/BlockMeta.sol new file mode 100644 index 0000000000..2a269b78a9 --- /dev/null +++ b/solidity/supra_contracts/src/BlockMeta.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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"; + +contract BlockMeta is OwnableUpgradeable, UUPSUpgradeable, IBlockMeta { + using LibUtils for address; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * STORAGE + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + + /// @notice Ordered list of functions to be executed + /// @dev Layout: [target[160] | selector[32] | 0[64]] + uint256[] private executions; + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * CONSTRUCTOR AND INITIALIZER + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the owner of the contract. + function initialize(address _initialOwner) public initializer { + __Ownable_init(_initialOwner); + } + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * ADMIN FUNCTIONS + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + */ + + /// @notice Registers a function selector. + /// @param _targetContract The target contract address. + /// @param _selector Function selector to be called on target contract. + function register(address _targetContract, bytes4 _selector) external onlyOwner { + _targetContract.validateContractAddress(); + + require(_selector != bytes4(0), InvalidSelector()); + + uint256 executionEntry = packExecution(_targetContract, _selector); + + // Check to prevent duplicate entries, reverts if already registered + checkDuplicate(executionEntry); + + // Add to the execution order + executions.push(executionEntry); + + emit SelectorRegistered(_targetContract, _selector); + } + + /// @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) | 0(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; + + // Clear existing array + delete executions; + + for (uint256 i = 0; i < inputCount; i++) { + uint256 inputExecution = _executions[i]; + (address target, bytes4 selector) = unpackExecution(inputExecution); + + // Input validation + target.validateContractAddress(); + require(selector != bytes4(0), InvalidSelector()); + + // Check to prevent duplicate entries, reverts if already registered + checkDuplicate(inputExecution); + + executions.push(inputExecution); + } + + emit ExecutionOrderUpdated(_executions); + } + + /// @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++) { + (address target, bytes4 selector) = unpackExecution(executions[i]); + + (bool ok, bytes memory data) = target.call(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 execution entry is already registered. + /// @param _executionEntry The packed execution entry to check. + function checkDuplicate(uint256 _executionEntry) private view { + uint256 len = executions.length; + for (uint256 i = 0; i < len; i++) { + if (executions[i] == _executionEntry) { + revert SelectorAlreadyRegistered(); + } + } + } + + /// @notice Finds the index of a given execution entry in the `executions` array. + /// @param _executionEntry The packed execution entry to search for. + /// @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] == _executionEntry) { + 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]; + + 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); + } + + + /** + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + * 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 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..b6403177f0 --- /dev/null +++ b/solidity/supra_contracts/src/Diamond.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +/******************************************************************************\ +* 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 {LibAppStorage} from "./libraries/LibAppStorage.sol"; +import {IDiamondCut} from "./interfaces/IDiamondCut.sol"; +import {IDiamondLoupe} from "./interfaces/IDiamondLoupe.sol"; +import {IFacetSelectors} from "./interfaces/IFacetSelectors.sol"; +import {DiamondInit} from "./upgradeInitializers/DiamondInit.sol"; +import {FacetsDeployment, InitParams} from "./libraries/DiamondTypes.sol"; +import { IERC173 } from "./interfaces/IERC173.sol"; +import { IERC165 } from "./interfaces/IERC165.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 Returns true if registry has been initialized. + function isInitialized() external view returns (bool) { + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + return ds.supportedInterfaces[type(IERC165).interfaceId] && + ds.supportedInterfaces[type(IDiamondCut).interfaceId] && + ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] && + ds.supportedInterfaces[type(IERC173).interfaceId]; + } + + /// @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..489b085c1e --- /dev/null +++ b/solidity/supra_contracts/src/ERC20Supra.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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..3d8e6b2abd --- /dev/null +++ b/solidity/supra_contracts/src/ERC20SupraHandler.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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..b189c8031e --- /dev/null +++ b/solidity/supra_contracts/src/MultiSignatureWallet.sol @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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; + + // Structure to hold transaction details + struct Transaction { + address to; // Transaction target address + uint64 timeout; // Expiry timestamp of the transaction + uint24 numConfirmations; // Number of confirmations received for 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; + + // 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 Helper function to remove a transaction and emit an event if it is expired. + /// @param _txIndex Index of the transaction. + /// @return bool True if the transaction was expired and removed. + function cleanupIfExpired(uint256 _txIndex) private returns (bool) { + if (transactions[_txIndex].timeout < block.timestamp) { + removeTransaction(_txIndex); + emit TransactionExpired(_txIndex); + + return true; + } + return false; + } + + /// @dev Helper function to remove a transaction from the 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 (confirmations[_txIndex].contains(msg.sender)) revert TxnAlreadyConfirmed(); + } + + /** + * @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; + } + + /** + * @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(); + + uint256 currentTxIndex = txIndex; + + transactions[currentTxIndex] = Transaction({ + to: _to, + timeout: uint64(block.timestamp) + _timeoutDuration, + //We assume the act of submission is an implicit confirmation + numConfirmations: 1, + value: _value, + data: _data + }); + + confirmations[currentTxIndex].add(msg.sender); + txIndex++; + txCount++; + + emit SubmitTransaction(msg.sender, currentTxIndex, _to, _value, _data); + } + + /** + * @dev Function to confirm an existing transaction. + * @dev If the transaction is expired, it is deleted and TransactionExpired is emitted. + * @param _txIndex Index of the transaction to confirm. + */ + function confirmTransaction(uint256 _txIndex) public { + onlyOwner(msg.sender); + txExists(_txIndex); + notConfirmed(_txIndex); + if (cleanupIfExpired(_txIndex)) { + // Transaction expired, action is no longer applicable + return; + } + Transaction storage transaction = transactions[_txIndex]; + transaction.numConfirmations += 1; + confirmations[_txIndex].add(msg.sender); + + emit ConfirmTransaction(msg.sender, _txIndex); + } + + /** + * @dev Function to execute a confirmed transaction. + * @dev If the transaction is expired, it is deleted and TransactionExpired is emitted. + * @param _txIndex Index of the transaction to execute. + */ + function executeTransaction(uint256 _txIndex) public returns (bytes memory) { + onlyOwner(msg.sender); + txExists(_txIndex); + if (cleanupIfExpired(_txIndex)) { + // Transaction expired, action is no longer applicable + return bytes(""); + } + Transaction memory transaction = transactions[_txIndex]; + if (transaction.numConfirmations < 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 If the transaction is expired, it is deleted and TransactionExpired is emitted. + * @param _txIndex Index of the transaction to revoke confirmation. + */ + function revokeConfirmation(uint256 _txIndex) external { + onlyOwner(msg.sender); + txExists(_txIndex); + if (cleanupIfExpired(_txIndex)) { + // Transaction expired, action is no longer applicable + return; + } + if (!confirmations[_txIndex].contains(msg.sender)) revert TransactionNotConfirmed(); + + Transaction storage transaction = transactions[_txIndex]; + + transaction.numConfirmations -= 1; + confirmations[_txIndex].remove(msg.sender); + + emit RevokeConfirmation(msg.sender, _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) + emit OwnersAdded(ownersToUpdate); + } + + /** + * @dev Function to remove existing owners from the wallet. + * @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)) { + ownersToUpdate[c++] = owner; + } + } + + if (owners.length() < numConfirmationsRequired) { + revert InvalidNumberOfConfirmations(); + } + + if (c > 0) + emit OwnersRemoved(ownersToUpdate); + } + + /** + * @dev Function to update the number of required confirmations for transactions. + * @param _numConfirmationsRequired New number of confirmations required for transactions. + */ + function updateNumConfirmations(uint256 _numConfirmationsRequired) external { + onlyMultiSig(); + if ( + _numConfirmationsRequired == 0 || + _numConfirmationsRequired > owners.length() + ) revert InvalidNumberOfConfirmations(); + numConfirmationsRequired = _numConfirmationsRequired; + emit NumConfirmationUpdated(_numConfirmationsRequired); + } + + /** + * @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) { + txExists(_txIndex); + return confirmations[_txIndex].contains(_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); + Transaction storage transaction = transactions[_txIndex]; + + return ( + transaction.to, + transaction.value, + transaction.numConfirmations, + 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 { + // 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); + } +} diff --git a/solidity/supra_contracts/src/MultisigBeacon.sol b/solidity/supra_contracts/src/MultisigBeacon.sol new file mode 100644 index 0000000000..d95f0bc60e --- /dev/null +++ b/solidity/supra_contracts/src/MultisigBeacon.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +/** + * @title MultisigBeacon + * @dev A beacon that stores the implementation address for multisig proxies. + * Admin can upgrade the implementation to a new version. + */ +contract MultisigBeacon is UpgradeableBeacon { + /** + * @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) {} +} diff --git a/solidity/supra_contracts/src/SupraContractsBindings.sol b/solidity/supra_contracts/src/SupraContractsBindings.sol new file mode 100644 index 0000000000..c80d113e04 --- /dev/null +++ b/solidity/supra_contracts/src/SupraContractsBindings.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 isAutomationEnabled() 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..ff2fc0a43e --- /dev/null +++ b/solidity/supra_contracts/src/facets/ConfigFacet.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 *; + + /// @dev State variables + AppStorage internal s; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 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(); + + 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(); + + require(s.authorizedAccounts.remove(_account), AddressDoesNotExist()); + emit AuthorizationRevoked(_account, block.timestamp); + } + + /// @notice Function to enable the task registration. + function enableRegistration() external { + LibDiamond.enforceIsContractOwner(); + + if (s.registrationEnabled) { revert AlreadyEnabled(); } + s.registrationEnabled = true; + + emit TaskRegistrationEnabled(s.registrationEnabled); + } + + /// @notice Function to disable the task registration. + function disableRegistration() external { + LibDiamond.enforceIsContractOwner(); + + 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); + 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. + function updateConfigBuffer( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint8 _congestionThresholdPercentage, + uint128 _congestionBaseFeeWeiPerSec, + uint8 _congestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) external { + LibDiamond.enforceIsContractOwner(); + + LibCommon.validateConfigParameters( + _taskDurationCapSecs, + _registryMaxGasCap, + _congestionThresholdPercentage, + _congestionExponent, + _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 + }); + s.configuration[LibAppStorage.BUFFER_CONFIG] = configBuffer; + s.ifBufferExists = true; + + registryState.nextCycleRegistryMaxGasCap = _registryMaxGasCap; + registryState.nextCycleSysRegistryMaxGasCap = _sysRegistryMaxGasCap; + + emit ConfigBufferUpdated(configBuffer); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the ERC20Supra address. + function erc20Supra() external view returns (address) { + return s.erc20Supra; + } + + /// @notice Returns if task registration is enabled. + function isRegistrationEnabled() external view returns (bool) { + return s.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(); + } + + function getSelectors() external pure override returns (bytes4[] memory selectors) { + selectors = new bytes4[](10); + 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; + } +} diff --git a/solidity/supra_contracts/src/facets/CoreFacet.sol b/solidity/supra_contracts/src/facets/CoreFacet.sol new file mode 100644 index 0000000000..8d05890a9c --- /dev/null +++ b/solidity/supra_contracts/src/facets/CoreFacet.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 {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; + + /// @dev State variables + AppStorage internal s; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VM FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Called by the VM Signer on `AutomationBookkeepingAction::Process` action emitted by native layer ahead of the cycle transition. + /// @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(); + + 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. + 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(); + + if (s.automationEnabled) { revert AlreadyEnabled(); } + + s.automationEnabled = true; + if (s.cycleState == LibCommon.CycleState.READY) { + LibCore.moveToStartedState(); + LibCore.updateConfigFromBuffer(); + } + + emit AutomationEnabled(s.automationEnabled); + } + + /// @notice Function to disable the automation. + function disableAutomation() external { + LibDiamond.enforceIsContractOwner(); + + if (!s.automationEnabled) { revert AlreadyDisabled(); } + + s.automationEnabled = false; + if (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) { + return (s.index, s.startTime, s.durationSecs, s.cycleState); + } + + /// @notice Returns the duration of the current cycle. + function getCycleDuration() external view returns (uint64) { + 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. + function getCycleStateDetails() external view returns (LibCommon.CycleDetails memory details) { + 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.uintSetToUint64Array(transitionState.expectedTasksToBeProcessed); + } + + /// @notice Returns if automation is enabled. + function isAutomationEnabled() external view returns (bool) { + return s.automationEnabled; + } + + /// @notice Removes registered tasks when predicate validation fails during runtime. + /// @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(); + + if (!s.automationEnabled) { return; } + if (s.index != cycleIndex) { revert LibCore.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[](10); + 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] = CoreFacet.isAutomationEnabled.selector; + selectors[9] = CoreFacet.getCycleStateDetails.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..d7b5a4b829 --- /dev/null +++ b/solidity/supra_contracts/src/facets/DiamondCutFacet.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* 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..55187aded2 --- /dev/null +++ b/solidity/supra_contracts/src/facets/DiamondLoupeFacet.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +/******************************************************************************\ +* 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"; + +// The functions in DiamondLoupeFacet MUST be added to a diamond. +// The EIP-2535 Diamond standard requires these functions. + +contract DiamondLoupeFacet is IDiamondLoupe, IERC165, IFacetSelectors { + + // 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]; + } + + function getSelectors() external pure override returns (bytes4[] memory s) { + s = new bytes4[](5); + 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; + } +} diff --git a/solidity/supra_contracts/src/facets/OwnershipFacet.sol b/solidity/supra_contracts/src/facets/OwnershipFacet.sol new file mode 100644 index 0000000000..a68d0ded76 --- /dev/null +++ b/solidity/supra_contracts/src/facets/OwnershipFacet.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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..e717a682de --- /dev/null +++ b/solidity/supra_contracts/src/facets/RegistryFacet.sol @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 *; + + /// @dev State variables + AppStorage internal s; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 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(s.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 cancelledTasks = new LibCommon.TaskCancelled[](_taskIndexes.length); + uint256 counter; + + for (uint256 i; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + cancelledTasks[counter++] = LibRegistry.cancelTask(taskId, false); + } + } + + if (counter > 0) { + 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 cancelledTasks = new LibCommon.TaskCancelled[](_taskIndexes.length); + uint256 counter; + + for (uint256 i; i < _taskIndexes.length; i++) { + uint64 taskId = _taskIndexes[i]; + if (LibCommon.ifTaskExists(taskId)) { + cancelledTasks[counter++] = LibRegistry.cancelTask(taskId, true); + } + } + + if (counter > 0) { + 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 stoppedTasks = 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 + ); + stoppedTasks[counter++] = ts; + totalRefundFee += refund; + } + } + + // Refund and emit event if any tasks were stopped + if (counter > 0) { + LibAccounting.refund(msg.sender, totalRefundFee); + + // 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 stoppedTasks = 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); + stoppedTasks[counter++] = ts; + } + } + + if (counter > 0) { + // Emit task stopped event + emit TasksStopped(stoppedTasks, msg.sender); + } + } + + /// @notice Helper function for validation. + function validateInput(uint64[] memory _taskIndexes) private view { + if (!s.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. + 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. + 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 s.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. + function getActiveTaskIds() external view returns (uint256[] memory) { + return LibAppStorage.registryState().activeTaskIds.values(); + } + + /// @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..23f69f0c3c --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IBlockMeta.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 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. + event SelectorRegistered(address indexed targetContract, bytes4 indexed selector); + + /// @notice Emitted when a function selector is removed from per-block execution. + /// @param targetContract Address of the target contract. + /// @param selector Deregistered function selector. + event SelectorDeregistered(address indexed targetContract, bytes4 indexed selector); + + /// @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 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. + function register(address _targetContract, bytes4 _selector) 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) | 0(64)]. + /// @param _executions Array of packed execution entries representing the new order. + function updateExecutionOrder(uint256[] calldata _executions) 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); +} \ 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..1c54630d62 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IConfigFacet.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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); + + + // ============================================================= + // 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); + + // ============================================================= + // 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, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) 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..1d5a1d3c2c --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/ICoreFacet.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 InvalidArrayLength(); + error InvalidRegistryState(); + + // ============================================================= + // 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 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..3ee934ada9 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IDiamondCut.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* 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..7f9f6a55d0 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IDiamondLoupe.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* 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..04b7bcc9ab --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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..a708048457 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC173.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @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 Set _newOwner to address(0) to renounce any ownership. + /// @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..87af2cf06f --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC20Supra.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +interface IERC20Supra { + /// @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..3462664537 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IERC20SupraHandler.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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..418b2e17d9 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IFacetSelectors.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @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..cb3b4f66d8 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IMultiSignatureWallet.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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(); + + // ── 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); + + // ── 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); + + // ── 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. Removes it if already expired. + /// @param _txIndex Index of the transaction to confirm. + function confirmTransaction(uint256 _txIndex) external; + + /// @notice Executes a transaction once enough confirmations are gathered. Removes it if expired. + /// @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. Removes the transaction if expired. + /// @param _txIndex Index of the transaction. + function revokeConfirmation(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 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 + ); +} \ 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..0e4114b849 --- /dev/null +++ b/solidity/supra_contracts/src/interfaces/IRegistryFacet.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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, + uint64 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, + uint64 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 AutomationNotEnabled(); + error CycleTransitionInProgress(); + error TaskIndexesCannotBeEmpty(); + error TransferFailed(); + error UnauthorizedAccount(); + + // ============================================================= + // 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/libraries/DiamondTypes.sol b/solidity/supra_contracts/src/libraries/DiamondTypes.sol new file mode 100644 index 0000000000..f9bab03c46 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/DiamondTypes.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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; + 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..b095ab0c82 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibAccounting.sol @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AppStorage, Config, LibAppStorage, RegistryState, TaskMetadata} from "./LibAppStorage.sol"; +import {LibCommon} from "./LibCommon.sol"; +import {IRegistryFacet} from "../interfaces/IRegistryFacet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.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; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ERRORS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + error ErrorCycleFeeRefund(); + error ErrorDepositRefund(); + error InsufficientBalanceForRefund(); + error InvalidCycleRefundFee(); + error RegisteredTaskInvalidType(); + error TransferFailed(); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 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, + uint64 _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 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; + 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, + uint64 _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 autoamtion fees of the task. + function refundTaskFees( + uint64 _currentTime, + uint64 _refundDuration, + uint128 _automationFeePerSec, + TaskMetadata 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, + uint64(_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 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 caller is not AutomationRegistry. + 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 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 + ); + + uint128 taskFeeForFullCycle = calculateAutomationFeeForInterval(s.durationSecs, _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 = taskFeeForFullCycle; + cycleFeeRefund = taskFeeForResidualTime / REFUND_FACTOR; + depositRefund = _depositFee; + } else { + cycleLockedFeeForTask = 0; + cycleFeeRefund = 0; + depositRefund = _depositFee / REFUND_FACTOR; + } + + bool result = safeUnlockLockedDeposit(_taskIndex, _depositFee); + if (!result) { revert ErrorDepositRefund(); } + + if (cycleLockedFeeForTask < cycleFeeRefund) { revert InvalidCycleRefundFee(); } + + (bool hasLockedFee, uint256 remainingCycleLockedFees ) = safeUnlockLockedCycleFee(registryState.cycleLockedFees, uint64(cycleLockedFeeForTask), _taskIndex); + if (!hasLockedFee) { revert 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..8211ec82ea --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibAppStorage.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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; +} + +/// @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; + EnumerableSet.UintSet expectedTasksToBeProcessed; +} + +/// @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 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; + EnumerableSet.UintSet activeTaskIds; + EnumerableSet.UintSet taskIdList; + EnumerableSet.UintSet sysTaskIds; + 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; + + // ============================================================= + // 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; + + function appStorage() internal pure returns (AppStorage storage s) { + assembly { + s.slot := 0 + } + } + + 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..9c43cb5dc2 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibCommon.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {AppStorage, LibAppStorage, RegistryState, TaskMetadata} 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 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, + 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 (_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 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(registryState.activeTaskIds.remove(_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..1506f19b69 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibCore.sol @@ -0,0 +1,704 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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, TransitionState} from "./LibAppStorage.sol"; +import {ICoreFacet} from "../interfaces/ICoreFacet.sol"; +import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +library LibCore { + using Arrays for uint256[]; + using LibUtils for address; + using EnumerableSet for EnumerableSet.UintSet; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CUSTOM ERRORS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + error InconsistentTransitionState(); + error InvalidInputCycleIndex(); + error InvalidRegistryState(); + error OutOfOrderTaskProcessingRequest(); + error TaskIndexNotFound(); + error TransferFailed(); + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: PRIVATE FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the number of total tasks. + function totalTasks() private view returns (uint256) { + return LibAppStorage.registryState().taskIdList.length(); + } + + /// @notice Returns all the automation tasks available in the registry. + function getTaskIdList() private view returns (uint256[] memory) { + return LibAppStorage.registryState().taskIdList.values(); + } + + /// @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; + registryState.sysGasCommittedForNextCycle = _sysGasCommittedForNextCycle; + registryState.sysGasCommittedForThisCycle = _sysGasCommittedForNextCycle; + registryState.gasCommittedForNextCycle = _gasCommittedForNextCycle; + registryState.gasCommittedForThisCycle = _gasCommittedForNewCycle; + + registryState.activeTaskIds.clear(); + if (_state == LibCommon.CycleState.FINISHED) { + uint256[] memory taskIds = registryState.taskIdList.values(); + for (uint256 i = 0; i < taskIds.length; i++) { + registryState.activeTaskIds.add(taskIds[i]); + } + } else { + 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. + function updateExpectedTasks(uint256[] memory _expectedTasks) private { + TransitionState storage transitionState = LibAppStorage.transitionState(); + transitionState.expectedTasksToBeProcessed.clear(); + + for (uint256 i = 0; i < _expectedTasks.length; i++) { + transitionState.expectedTasksToBeProcessed.add(_expectedTasks[i]); + } + } + + /// @notice Transitions cycle state to the READY state. + function moveToReadyState() private { + // If the cycle duration updated has been identified during transtion, 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 disbaled 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 + transitionState.expectedTasksToBeProcessed.clear(); + 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; + transitionState.expectedTasksToBeProcessed.clear(); + } + } + 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 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 InconsistentTransitionState(); } + uint64 expectedTask = uint64(transitionState.expectedTasksToBeProcessed.at(nextTaskIndexPosition)); + + if (expectedTask != _taskIndex) { revert 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 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.values(); + 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); + uint64 currentCycleEndTime = currentTime + LibAppStorage.transitionState().newCycleDuration; + + // Sort task indexes to charge automation fees in their chronological order + uint256[] memory taskIndexes = _taskIndexes.sort(); + + 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; + } + } + + 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. If the task is already processed or missing from the registry then nothing is done. + /// @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) { + if (LibCommon.ifTaskExists(_taskIndex)) { + markTaskProcessed(_taskIndex); + + TaskMetadata memory task = LibCommon.getTask(_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 + + 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 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 (_taskIndexes.length == 0) { return; } + + if (s.cycleState != LibCommon.CycleState.FINISHED) { revert InvalidRegistryState(); } + + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert InvalidRegistryState(); } + if (s.index + 1 != _cycleIndex) { revert 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 (_taskIndexes.length == 0) { return; } + + if (s.cycleState != LibCommon.CycleState.SUSPENDED) { revert InvalidRegistryState(); } + if (s.index != _cycleIndex) { revert InvalidInputCycleIndex(); } + // Check if transition state exists + if (!s.ifTransitionStateExists) { revert InvalidRegistryState(); } + + uint64 currentTime = uint64(block.timestamp); + + // Sort task indexes as order is important + uint256[] memory taskIndexes = _taskIndexes.sort(); + uint64[] memory removedTasks = new uint64[](taskIndexes.length); + + uint64 removedCounter; + for (uint i = 0; i < taskIndexes.length; i++) { + uint64 taskId = uint64(taskIndexes[i]); + if (LibCommon.ifTaskExists(taskId)) { + TaskMetadata memory task = LibCommon.getTask(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(); + 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(); + + TaskMetadata memory task = 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 { + uint256[] memory expectedTasksToBeProcessed = getTaskIdList().sort(); + + // 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; + 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 InvalidRegistryState(); } + if (currentTime >= cycleEndTime) { revert InvalidRegistryState(); } + if (!LibCommon.isCycleStarted()) { revert InvalidRegistryState(); } + + uint256[] memory tasksIdList = getTaskIdList(); + uint256[] memory expectedTasksToBeProcessed = tasksIdList.sort(); + + 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; + + updateExpectedTasks(expectedTasksToBeProcessed); + s.ifTransitionStateExists = true; + + updateCycleStateTo(LibCommon.CycleState.SUSPENDED); + } else { + if (s.cycleState != LibCommon.CycleState.FINISHED) { revert InvalidRegistryState(); } + if (isTransitionInProgress()) { revert 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) { + s.durationSecs = LibAppStorage.transitionState().newCycleDuration; + s.ifTransitionStateExists = false; + } + + 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; + } + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/libraries/LibDiamond.sol b/solidity/supra_contracts/src/libraries/LibDiamond.sol new file mode 100644 index 0000000000..bb8cdb392a --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibDiamond.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* 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; + } + + 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 { + 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(); + } + } + emit DiamondCut(_diamondCut, _init, _calldata); + initializeDiamondCut(_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++; + } + } + + function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal { + assertNonEmptySelectors(_functionSelectors); + DiamondStorage storage ds = diamondStorage(); + // if function does not exist then do nothing and return + 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..6cecb3ecb9 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibDiamondUtils.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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, + taskCapacity: 400, + cycleDurationSecs: 1200, + sysTaskDurationCapSecs: 3600 * 24 * 180, + sysRegistryMaxGasCap: 20_000_000, + sysTaskCapacity: 100, + 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 = deploy_facets(); + d.diamond = address (new Diamond(_owner, d.facets, _erc20Supra, _params)); + } + + /// @notice Deploys all facets, DiamondInit. + function deploy_facets() 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..ec8cffae84 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibRegistry.sol @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {LibAccounting} from "./LibAccounting.sol"; +import {LibCommon} from "./LibCommon.sol"; +import {LibUtils} from "./LibUtils.sol"; +import {AppStorage, Config, LibAppStorage, RegistryState, TaskMetadata} from "./LibAppStorage.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; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ERRORS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + error AlreadyCancelled(); + error RegistrationDisabled(); + error AutomationNotEnabled(); + error CycleTransitionInProgress(); + error ErrorDepositRefund(); + error FailedToCallTxHashPrecompile(); + error TxnHashLengthShouldBe32(uint64); + error InvalidMaxGasAmount(); + error GasCommittedExceedsMaxGasCap(); + error GasCommittedValueUnderflow(); + error InsufficientFeeCapForCycle(uint128 estimatedAutomationFeeForCycle); + error InvalidExpiryTime(); + error InvalidGasPriceCap(); + error InvalidTaskDuration(); + error TaskCapacityReached(); + error TaskExpiresBeforeNextCycle(); + error TaskIndexNotFound(); + error TaskIndexNotUnique(); + error UnauthorizedAccount(); + error UnsupportedTaskOperation(); + error StaticCallToPredicateFailed(); + error InvalidPayloadLength(); + error InvalidReturnLengthOfPredicate(); + error InvalidReturnTypeOfPredicate(); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 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 InvalidExpiryTime(); } + + uint64 taskDuration = _expiryTime - _regTime; + if (taskDuration > _taskDurationCap) { revert InvalidTaskDuration(); } + + if ( _expiryTime <= _cycleEndTime) { revert TaskExpiresBeforeNextCycle(); } + } + + /// @notice Helper function to validate the inputs while registering a task. + function validateInputs(bytes memory _payloadTx, uint128 _maxGasAmount) private view { + ( , address payloadTarget, bytes memory payload, ) = abi.decode(_payloadTx, (uint128, address, bytes, LibCommon.AccessListEntry[])); + payloadTarget.validateContractAddress(); + if (payload.length < 4) revert InvalidPayloadLength(); + + if (_maxGasAmount == 0) { revert InvalidMaxGasAmount(); } + } + + /// @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, FailedToCallTxHashPrecompile()); + require(out.length == 32, 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 UnauthorizedAccount(); } + + // Enforce task type + if (_isGst) { + if (_taskType == LibCommon.TaskType.UST) { + revert UnsupportedTaskOperation(); + } + } else { + if (_taskType == LibCommon.TaskType.GST) { + revert UnsupportedTaskOperation(); + } + } + } + + /// @notice Validates a predicate by calling it and checking the return value. + /// @param _predicate Predicate to validate + function validatePredicate(bytes memory _predicate) private view { + (address payloadTarget, bytes memory payload) = abi.decode(_predicate, (address, bytes)); + payloadTarget.validateContractAddress(); + if (payload.length < 4) revert InvalidPayloadLength(); + + (bool success, bytes memory data) = payloadTarget.staticcall(payload); + if (!success) revert StaticCallToPredicateFailed(); + if (data.length != 32) revert InvalidReturnLengthOfPredicate(); + + uint256 val = abi.decode(data, (uint256)); + if (val > 1) revert 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 AutomationNotEnabled(); } + if (!s.registrationEnabled) { revert RegistrationDisabled(); } + + if (!LibCommon.isCycleStarted()) { revert CycleTransitionInProgress(); } + + validatePredicate(_predicate); + + uint64 taskDurationCap; + uint128 gasCommittedForNextCycle; + uint128 nextCycleRegistryMaxGasCap; + if (_isUst) { + if (_totalTasks >= activeConfig.taskCapacity) { revert TaskCapacityReached(); } + if (_gasPriceCap == 0) { revert InvalidGasPriceCap(); } + + gasCommittedForNextCycle = registryState.gasCommittedForNextCycle; + uint128 estimatedAutomationFeeForCycle = LibAccounting.estimateAutomationFeeWithCommittedOccupancyInternal(_maxGasAmount, gasCommittedForNextCycle); + if (_automationFeeCapForCycle < estimatedAutomationFeeForCycle) { revert InsufficientFeeCapForCycle(estimatedAutomationFeeForCycle); } + taskDurationCap = activeConfig.taskDurationCapSecs; + nextCycleRegistryMaxGasCap = registryState.nextCycleRegistryMaxGasCap; + } else { + if (_totalTasks >= activeConfig.sysTaskCapacity) { revert TaskCapacityReached(); } + + gasCommittedForNextCycle = registryState.sysGasCommittedForNextCycle; + taskDurationCap = activeConfig.sysTaskDurationCapSecs; + nextCycleRegistryMaxGasCap = registryState.nextCycleSysRegistryMaxGasCap; + } + + validateTaskDuration(_regTime, _expiryTime, taskDurationCap, s.startTime + s.durationSecs); + validateInputs(_payloadTx, _maxGasAmount); + + uint128 gasCommitted = _maxGasAmount + gasCommittedForNextCycle; + if (gasCommitted > nextCycleRegistryMaxGasCap) { revert 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), TaskIndexNotUnique()); + require(registryState.addressToTasks[msg.sender].add(taskIndex), TaskIndexNotUnique()); + + if (!_isUst) { + require(registryState.sysTaskIds.add(taskIndex), 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 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 totalTasks = isUst ? registryState.taskIdList.length() : registryState.sysTaskIds.length(); + + 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(); + + TaskMetadata memory task = registryState.tasks[_taskIndex]; + + validateOwnerType(task.owner, task.taskType, _isGst); + if (task.taskState == LibCommon.TaskState.CANCELLED) { revert 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 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(); + TaskMetadata memory task = 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..e3db9eb258 --- /dev/null +++ b/solidity/supra_contracts/src/libraries/LibUtils.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +// 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 an EnumerableSet.UintSet to a uint64 array. + /// @param set The UintSet to convert. + /// @return result The values as a uint64 array. + function uintSetToUint64Array(EnumerableSet.UintSet storage set) internal view returns (uint64[] memory result) { + uint256 length = EnumerableSet.length(set); + result = new uint64[](length); + for (uint256 i = 0; i < length; i++) { + result[i] = uint64(EnumerableSet.at(set, 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..55aedf1b94 --- /dev/null +++ b/solidity/supra_contracts/src/upgradeInitializers/DiamondInit.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/******************************************************************************\ +* 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 { 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"; + +/// @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. +/// +/// This initializer performs the following actions: +/// - Registers supported interfaces for ERC-165, IDiamondCut, IDiamondLoupe, and ERC-173. +/// - Sets the active registry configuration, protocol feature flags and trusted addresses. +/// - Establishes initial automation cycle state, index, and timestamp. +contract DiamondInit { + AppStorage internal s; + + /// @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 { + // Adding ERC165 data + 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; + + + LibCommon.validateConfigParameters( + _params.taskDurationCapSecs, + _params.registryMaxGasCap, + _params.congestionThresholdPercentage, + _params.congestionExponent, + _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 + }); + + s.configuration[LibAppStorage.ACTIVE_CONFIG] = activeConfig; + + s.automationEnabled = _params.automationEnabled; + s.registrationEnabled = _params.registrationEnabled; + s.erc20Supra = _erc20Supra; + + // --------------------------------------------------------------------- + // 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; + } + +} diff --git a/solidity/supra_contracts/submit_governance_action.sh b/solidity/supra_contracts/submit_governance_action.sh new file mode 100644 index 0000000000..296a6d70cb --- /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, which is currently "Blue!Tiger99@Moon.PROFILE" +# +# - 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/BaseDiamondTest.t.sol b/solidity/supra_contracts/test/BaseDiamondTest.t.sol new file mode 100644 index 0000000000..b9ded52187 --- /dev/null +++ b/solidity/supra_contracts/test/BaseDiamondTest.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {Deployment, InitParams, LibDiamondUtils} from "../src/libraries/LibDiamondUtils.sol"; +import {LibCommon} from "../src/libraries/LibCommon.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. + function registerUst() internal { + 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), + 2, + auxData + ); + vm.stopPrank(); + } + + /// @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); + } +} diff --git a/solidity/supra_contracts/test/BlockMeta.t.sol b/solidity/supra_contracts/test/BlockMeta.t.sol new file mode 100644 index 0000000000..c972bbcbcc --- /dev/null +++ b/solidity/supra_contracts/test/BlockMeta.t.sol @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 {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); + 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(); + } + + /// @dev Helper function to register a selector. + /// @param _targetContract The target contract address. + /// @param _selector Function selector to register. + function register(address _targetContract, bytes4 _selector) private { + vm.prank(admin); + blockMeta.register(_targetContract, _selector); + } + + /// @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); + + register(counterAddress, selector); + + (targets, selectors) = blockMeta.getExecutions(); + assertEq(targets.length, 1); + assertEq(selectors.length, 1); + assertEq(targets[0], counterAddress); + assertEq(selectors[0], selector); + } + + /// @dev Test to ensure 'register' emits event 'SelectorRegistered'. + function testRegisterEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit IBlockMeta.SelectorRegistered(counterAddress, selector); + + 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); + } + + /// @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 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); + + 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); + } + + /// @dev Test to ensure 'deregister' emits event 'SelectorDeregistered'. + function testDeregisterEmitsEvent() public { + testRegister(); + + vm.expectEmit(true, true, false, false); + emit IBlockMeta.SelectorDeregistered(counterAddress, selector); + + 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, false, false); + emit IBlockMeta.SelectorDeregistered(counterAddress, selector); + + 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); + executionOrder[1] = packExecution(counterAddress, selector); + + 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); + executionOrder[1] = packExecution(address(0), selector); + + 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); + executionOrder[1] = packExecution(alice, selector); + + 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); + executionOrder[1] = packExecution(counterAddress, bytes4(0)); + + vm.expectRevert(IBlockMeta.InvalidSelector.selector); + + vm.prank(admin); + 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); + executionOrder[1] = packExecution(counterAddress, selector); + + 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); + + 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 '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 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 Helper function to pack a target contract address and function selector into a single uint256 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); + } + + /// @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); + executionOrder[1] = packExecution(counterAddress, selector); + + 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..a92e2c4324 --- /dev/null +++ b/solidity/supra_contracts/test/ConfigFacet.t.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseDiamondTest} 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 {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(); + + 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(); + + 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(); + + vm.expectEmit(true, true, false, false); + emit IConfigFacet.RegistryFeeWithdrawn(admin, 0.002 ether); + + vm.prank(admin); + IConfigFacet(diamondAddr).withdrawFees(0.002 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 + }); + } + + /// @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.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.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.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.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } +} \ 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..75cdd4bb6f --- /dev/null +++ b/solidity/supra_contracts/test/CoreFacet.t.sol @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseDiamondTest} from "./BaseDiamondTest.t.sol"; +import {IRegistryFacet} from "../src/interfaces/IRegistryFacet.sol"; +import {ICoreFacet} from "../src/interfaces/ICoreFacet.sol"; +import {LibCommon} from "../src/libraries/LibCommon.sol"; +import {LibUtils} from "../src/libraries/LibUtils.sol"; +import {LibCore} from "../src/libraries/LibCore.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, + 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 READY if automation is disabled and no tasks exist. + function testMonitorCycleEndWhenAutomationDisabledNoTasks() public { + // Disable automation + vm.prank(admin); + ICoreFacet(diamondAddr).disableAutomation(); + + assertFalse(ICoreFacet(diamondAddr).isAutomationEnabled()); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, false, true); + emit ICoreFacet.AutomationCycleEvent( + indexBefore, + LibCommon.CycleState.READY, + 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.READY)); + } + + /// @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, false, 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(); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, LibCommon.CycleState stateBefore) = ICoreFacet(diamondAddr).getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, false, 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(); + + ( , 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; + + uint256[] memory activeTasks = new uint256[](1); + tasks[0] = 0; + + vm.deal(alice, 200 ether); + vm.prank(alice); + erc20SupraHandler.deposit{value: 100 ether}(); + + vm.expectEmit(true, false, false, false); + emit ICoreFacet.ActiveTasks(activeTasks); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index + 1, tasks); + + (uint64 newIndex, uint64 newStart, uint64 newDuration, LibCommon.CycleState newState) = ICoreFacet(diamondAddr).getCycleInfo(); + assertEq(newIndex, index + 1); + assertEq(newStart, uint64(block.timestamp)); + assertEq(newDuration, 1200); + assertEq(uint8(newState), uint8(LibCommon.CycleState.STARTED)); + + assertEq(IRegistryFacet(diamondAddr).getActiveTaskIds(), activeTasks); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForCurrentCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 0); + 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(); + + ( , 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(LibCore.InvalidInputCycleIndex.selector); + + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(index, tasks); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is disabled. + function testProcessTasksWhenCycleStateSuspendedAutomationDisabled() public { + registerUst(); + + ( , 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 Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is enabled. + function testProcessTasksWhenCycleStateSuspendedAutomationEnabled() public { + registerUst(); + + ( , 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(); + + ( , 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(LibCore.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 '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(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: 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 'removeRegisteredTask' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'removeRegisteredTask' removes a UST when predicate validation fails. + function testRemoveRegisteredTasksForUST() public { + // Register a UST + registerUst(); + + assertTrue(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 1); + + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 100_000); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 60.1 ether); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 61.1 ether); + assertEq(erc20Supra.balanceOf(alice), 38.9 ether); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + + vm.warp(1201); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(2, taskIndexes); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 3 ether); + + // Remove task due to predicate failure, cycle index is 2 + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + vm.stopPrank(); + + // Verify task is removed + assertFalse(IRegistryFacet(diamondAddr).ifTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getGasCommittedForNextCycle(), 0); + assertEq(IRegistryFacet(diamondAddr).getTotalDepositedAutomationFees(), 0); + assertEq(IRegistryFacet(diamondAddr).getCycleLockedFees(), 0 ether); + assertEq(erc20Supra.balanceOf(diamondAddr), 3.9375 ether); + assertEq(erc20Supra.balanceOf(alice), 96.0625 ether); + } + + /// @dev Test to ensure 'removeRegisteredTask' removes a GST when predicate validation fails. + function testRemoveRegisteredTasksForGST() public { + // Register a GST + 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 + ); + + assertTrue(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 1); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 100_000); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + vm.warp(1201); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(2, taskIndexes); + + // Remove task due to predicate failure + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + vm.stopPrank(); + + // Verify task is removed + assertFalse(IRegistryFacet(diamondAddr).ifSysTaskExists(0)); + assertEq(IRegistryFacet(diamondAddr).totalSystemTasks(), 0); + assertEq(IRegistryFacet(diamondAddr).getSystemGasCommittedForNextCycle(), 100_000); + } + + /// @dev Test to ensure 'removeRegisteredTask' emits 'TaskRemovedBySystem' event. + function testRemoveRegisteredTasksEmitsEvent() public { + registerUst(); + + uint256[] memory taskIndexes = new uint256[](1); + taskIndexes[0] = 0; + uint64[] memory tasksUint64 = new uint64[](1); + tasksUint64[0] = 0; + string memory reason = "Predicate failed"; + + vm.warp(1201); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(2, 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 + ICoreFacet(diamondAddr).removeRegisteredTask(2, tasksUint64[0], reason); + vm.stopPrank(); + } + + /// @dev Test to ensure 'removeRegisteredTask' reverts if caller is not VM Signer. + function testRemoveRegisteredTasksRevertsIfNotVmSigner() public { + registerUst(); + + 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(); + + vm.expectRevert(LibCore.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(); + + vm.expectRevert(LibCore.InvalidInputCycleIndex.selector); + + uint64 taskIndex = 0; + string memory reason = "Predicate failed"; + + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).removeRegisteredTask(0, taskIndex, reason); + } + +} diff --git a/solidity/supra_contracts/test/Counter.sol b/solidity/supra_contracts/test/Counter.sol new file mode 100644 index 0000000000..c390750340 --- /dev/null +++ b/solidity/supra_contracts/test/Counter.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 is_not_divisible_by_3() external view returns (bool) { + return counter % 3 != 0; + } + + /// @notice Updates the counter to a new value. + /// @param new_value New value for the counter. + /// Used during testing to register trigger automation task execution by making is_not_divisible_by_3 condition to be true. + function update(uint256 new_value) external { + if (msg.sender == privilegedAddress) { + counter = new_value; + } + } + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 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/DiamondInit.t.sol b/solidity/supra_contracts/test/DiamondInit.t.sol new file mode 100644 index 0000000000..4b76548d3f --- /dev/null +++ b/solidity/supra_contracts/test/DiamondInit.t.sol @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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, Deployment, 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 {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"; + +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, 400); + assertEq(config.sysTaskCapacity, 100); + assertEq(config.congestionThresholdPercentage, 50); + assertEq(config.congestionExponent, 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)); + } + + /// @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 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 '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 initialization fails if ERC20Supra address is zero. + function testInitializeRevertsIfErc20SupraIsZero() public { + vm.startPrank(admin); + FacetsDeployment memory facets = LibDiamondUtils.deploy_facets(); + 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.deploy_facets(); + 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.deploy_facets(); + + 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, + 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.deploy_facets(); + + 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, + 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.deploy_facets(); + + 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, + 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.deploy_facets(); + + 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, + 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.deploy_facets(); + + 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, + 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.deploy_facets(); + 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, + 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.deploy_facets(); + 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, + 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.deploy_facets(); + 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, + 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.deploy_facets(); + 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, + 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(); + } +} + +interface INonExistent { + function nonExistent() external; +} + +contract MockRegistryFacet { + function erc20Supra() external pure returns (address) { + return address(0x999); + } + + function counter() external pure returns (uint256) { + return 1; + } +} diff --git a/solidity/supra_contracts/test/ERC20Supra.t.sol b/solidity/supra_contracts/test/ERC20Supra.t.sol new file mode 100644 index 0000000000..d4dda1cc6e --- /dev/null +++ b/solidity/supra_contracts/test/ERC20Supra.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 {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)); + } +} \ 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..9e02a78ca2 --- /dev/null +++ b/solidity/supra_contracts/test/ERC20SupraHandler.t.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +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 {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 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); + } +} + +/// @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/MultiSignatureWallet.t.sol b/solidity/supra_contracts/test/MultiSignatureWallet.t.sol new file mode 100644 index 0000000000..5c78d39519 --- /dev/null +++ b/solidity/supra_contracts/test/MultiSignatureWallet.t.sol @@ -0,0 +1,857 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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' removes the tx and emits 'TransactionExpired' if transaction has expired. + function testConfirmTransactionRemovesTxIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + confirmTransaction(address(1005), 0); + assertEq(multiSig.txCount(), 0); + } + + /// @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' removes the tx and emits 'TransactionExpired' if the transaction has expired. + function testRevokeConfirmationRemovesTxIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + revokeConfirmation(address(1001), 0); + assertEq(multiSig.txCount(), 0); + } + + /// @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 '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' removes the tx and emits 'TransactionExpired' if transaction has expired. + function testExecuteTransactionRemovesTxIfExpired() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 0); + } + + /// @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 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' removes the tx and emits 'TransactionExpired' if transaction has expired. + function testAddOwnersRemovesTxIfExpired() public { + vm.warp(500); + submitTransactionToMultiSig(dataToAddOwnerInMultiSig()); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(0); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 0); + } + + /// @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 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' 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' removes the tx and emits 'TransactionExpired' if transaction has expired. + function testRemoveOwnersRemovesTxIfExpired() public { + testAddOwners(); + + vm.warp(500); + submitTransactionToMultiSig(dataToRemoveOwnerFromMultiSig()); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(1); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(1); + + vm.prank(address(1002)); + multiSig.executeTransaction(1); + assertEq(multiSig.txCount(), 0); + } + + /// @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' removes the tx and emits 'TransactionExpired' if the transaction has expired. + function testUpdateNumConfimationsRemovesTxIfExpired() public { + vm.warp(500); + submitTransactionToMultiSig(dataToUpdateNumConfimationsMultiSig(3)); + assertEq(multiSig.txCount(), 1); + + grantSufficientConfirmations(0); + + vm.warp(10501); + vm.expectEmit(true, false, false, false); + emit IMultiSignatureWallet.TransactionExpired(0); + + vm.prank(address(1002)); + multiSig.executeTransaction(0); + assertEq(multiSig.txCount(), 0); + } + + /// @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 expired transaction is removed and accessing it results in a revert. + function testGetTransactionRevertsIfTxExpiredAndCleanedUp() public { + vm.warp(500); + testSubmitTransactionIncrement(); + + vm.warp(10501); + confirmTransaction(address(1005), 0); + assertEq(multiSig.txCount(), 0); + + vm.expectRevert(IMultiSignatureWallet.InvalidTxnId.selector); + multiSig.getTransaction(0); + } +} diff --git a/solidity/supra_contracts/test/RegistryFacet.t.sol b/solidity/supra_contracts/test/RegistryFacet.t.sol new file mode 100644 index 0000000000..0d6448f8c5 --- /dev/null +++ b/solidity/supra_contracts/test/RegistryFacet.t.sol @@ -0,0 +1,1213 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {BaseDiamondTest} 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 {LibRegistry} from "../src/libraries/LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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 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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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(LibRegistry.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' 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(LibRegistry.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( + LibRegistry.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(LibRegistry.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' 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 '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(); + + 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(bob); + 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 registration is disabled. + function testRegisterSystemTaskRevertsIfRegistrationDisabled() public { + 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(LibRegistry.RegistrationDisabled.selector); + + vm.prank(bob); + 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 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(LibRegistry.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(LibRegistry.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(LibRegistry.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); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: 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(LibRegistry.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); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: 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 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(LibRegistry.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}(); + + vm.warp(1201); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(2, taskIndexes); + vm.stopPrank(); + + 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}(); + + vm.warp(1201); + vm.startPrank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + ICoreFacet(diamondAddr).processTasks(2, taskIndexes); + vm.stopPrank(); + + 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); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: 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(LibRegistry.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; + + vm.warp(1201); + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(2, 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(), 100000); + } + + /// @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; + + vm.warp(1201); + vm.prank(LibUtils.VM_SIGNER, LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).monitorCycleEnd(); + + vm.prank(LibUtils.VM_SIGNER); + ICoreFacet(diamondAddr).processTasks(2, 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); + } +} \ No newline at end of file diff --git a/solidity/supranova b/solidity/supranova new file mode 160000 index 0000000000..e288044902 --- /dev/null +++ b/solidity/supranova @@ -0,0 +1 @@ +Subproject commit e2880449024ff40a8155358a793fadff5e258eb4