diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04ec6c45..15589b5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,50 @@ on: - main jobs: + changes: + name: Detect changed contract areas + runs-on: core + outputs: + legacy: ${{ steps.filter.outputs.legacy }} + standards: ${{ steps.filter.outputs.standards }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: filter + shell: bash + run: | + set -euo pipefail + + legacy=false + standards=false + + if [[ "${{ github.event_name }}" == "push" ]]; then + legacy=true + standards=true + else + files="$(git diff --name-only \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}")" + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + case "$file" in + genesis/*|tests/*|bin/*|scripts/setup-compiler.sh|scripts/download-rusk.sh|scripts/build-test-contracts.sh) + legacy=true + ;; + esac + case "$file" in + standards/*|docs/dusk-contract-standards*|scripts/dusk-contract-standards-*|Cargo.toml|Cargo.lock|Makefile|README.md|rust-toolchain.toml|.github/workflows/ci.yml|.github/workflows/standards-hardening.yml) + standards=true + ;; + esac + done <<< "$files" + fi + + echo "legacy=$legacy" >> "$GITHUB_OUTPUT" + echo "standards=$standards" >> "$GITHUB_OUTPUT" + dusk_analyzer: name: Dusk Analyzer runs-on: core @@ -13,9 +57,12 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - run: cargo install --git https://github.com/dusk-network/cargo-dusk-analyzer - run: cargo dusk-analyzer + - run: cargo dusk-analyzer --manifest-path standards/Cargo.toml clippy: name: Clippy check + needs: changes + if: needs.changes.outputs.legacy == 'true' runs-on: core steps: - uses: actions/checkout@v4 @@ -25,9 +72,22 @@ jobs: test: name: Run Tests + needs: changes + if: needs.changes.outputs.legacy == 'true' runs-on: core steps: - uses: actions/checkout@v4 - uses: dsherret/rust-toolchain-file@v1 - run: make keys - run: make test + + standards: + name: Standards CI + needs: changes + if: needs.changes.outputs.standards == 'true' + runs-on: core + steps: + - uses: actions/checkout@v4 + - uses: dsherret/rust-toolchain-file@v1 + - run: rustup target add wasm32-unknown-unknown + - run: make standards-ci diff --git a/.github/workflows/standards-hardening.yml b/.github/workflows/standards-hardening.yml new file mode 100644 index 00000000..65423da4 --- /dev/null +++ b/.github/workflows/standards-hardening.yml @@ -0,0 +1,31 @@ +name: Standards Hardening + +on: + workflow_dispatch: + inputs: + property_cases: + description: Property-test cases per invariant + required: false + default: "8192" + data_driver_cases: + description: Forge data-driver fuzz cases + required: false + default: "2048" + schedule: + - cron: "17 2 * * *" + +jobs: + hardening: + name: Standards property and ABI fuzzing + runs-on: core + timeout-minutes: 240 + env: + STANDARDS_PROPTEST_CASES: ${{ github.event.inputs.property_cases || '8192' }} + STANDARDS_PROPTEST_MAX_SHRINK_ITERS: "16384" + STANDARDS_DATA_DRIVER_FUZZ_CASES: ${{ github.event.inputs.data_driver_cases || '2048' }} + STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS: "4096" + steps: + - uses: actions/checkout@v4 + - uses: dsherret/rust-toolchain-file@v1 + - run: rustup target add wasm32-unknown-unknown + - run: make standards-hardening diff --git a/Cargo.lock b/Cargo.lock index d4dbc29f..7ddf47c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -55,16 +55,16 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", @@ -103,15 +103,24 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] [[package]] name = "arbitrary" @@ -306,9 +315,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bincode" @@ -321,9 +330,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -348,26 +357,26 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.4.2", ] [[package]] name = "blake3" -version = "1.5.4" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.3.1", ] [[package]] @@ -381,9 +390,9 @@ dependencies = [ [[package]] name = "bls12_381-bls" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38440ec581131c35809ddb8554f63ede8a051d238cfe974ff21c5aa6ee47a36" +checksum = "6478fd9a5bbd62d60abf5a589cd68ad6c57d0ea592c2c93b26643fe9e5873883" dependencies = [ "bytecheck", "dusk-bls12_381 0.13.0", @@ -421,9 +430,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -453,6 +462,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cast" version = "0.3.0" @@ -461,18 +476,19 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "charlie" @@ -521,18 +537,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.18" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -540,9 +556,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "console" @@ -573,11 +589,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -738,9 +760,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -757,15 +779,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crumbles" -version = "0.3.1" +version = "0.3.2-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e660ac9c1d7549c850b4cbf31c06bd7e452879b55296554d5c85e25a26fefe" +checksum = "d67910efb2241f4d8f2a184c18db43be0f916b0f85e742376c6b3c9614455b62" dependencies = [ "libc", "rangemap", @@ -779,9 +801,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core", @@ -852,13 +874,13 @@ dependencies = [ [[package]] name = "dlmalloc" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3264b043b8e977326c1ee9e723da2c1f8d09a99df52cacf00b4dbce5ac54414d" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" dependencies = [ "cfg-if", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -936,9 +958,9 @@ dependencies = [ [[package]] name = "dusk-jubjub" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0e6995ff48af44bbcb810e4a49ed4ddb0800601c9c9722fd00d7f7e0b77c3a" +checksum = "49c388e710446aabbe7c7717109a239cea254594253ceb94e9134976c5b59509" dependencies = [ "bitvec", "blake2b_simd", @@ -1065,7 +1087,7 @@ dependencies = [ "indexmap", "libc", "log", - "object", + "object 0.33.0", "once_cell", "paste", "rayon", @@ -1077,7 +1099,7 @@ dependencies = [ "wasmparser 0.202.0", "wasmtime-jit-icache-coherence", "wasmtime-slab", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1097,7 +1119,7 @@ dependencies = [ "dusk-wasmtime-environ", "gimli", "log", - "object", + "object 0.33.0", "target-lexicon", "thiserror", "wasmparser 0.202.0", @@ -1116,7 +1138,7 @@ dependencies = [ "gimli", "indexmap", "log", - "object", + "object 0.33.0", "paste", "serde", "serde_derive", @@ -1149,14 +1171,14 @@ dependencies = [ "wasmtime-asm-macros", "wasmtime-slab", "wasmtime-versioned-export-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "either" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -1166,9 +1188,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -1177,7 +1199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1188,20 +1210,26 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core", "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foldhash" version = "0.1.5" @@ -1214,6 +1242,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1226,9 +1278,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1237,14 +1289,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -1281,12 +1334,13 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1313,7 +1367,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", ] [[package]] @@ -1322,7 +1376,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", ] [[package]] @@ -1339,15 +1393,21 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1381,6 +1441,12 @@ dependencies = [ "dusk-core", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.11.4" @@ -1388,29 +1454,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1442,16 +1508,18 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1486,9 +1554,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -1499,19 +1567,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", ] @@ -1523,15 +1596,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" @@ -1553,9 +1626,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memfd" @@ -1563,7 +1636,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -1616,12 +1689,12 @@ dependencies = [ [[package]] name = "msgpacker-derive" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e926eca5ac0d976018120667ab61ee8a1cff4afdcd202357971b610e7ff57b5a" +checksum = "c334ab77a6d4f29c5c2e41149c0f03af4082923aa364aa0c9876232f33f9853f" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -1664,11 +1737,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +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 = "oorandom" @@ -1721,7 +1803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b45ed942d9d166f7a45d16630529d68d68cfde7f37191d4646dfbe4b350b9cc" dependencies = [ "aes-gcm", - "bls12_381-bls 0.4.0", + "bls12_381-bls 0.4.1", "bytecheck", "dusk-bls12_381 0.14.2", "dusk-bytes", @@ -1740,13 +1822,14 @@ dependencies = [ [[package]] name = "piecrust" -version = "0.29.0-rc.3" +version = "0.29.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c38e9e4914aed3e4ce5e5c8d48df1114e675879c20055064e00608893ca2c6" +checksum = "ab8d02351245508a0d2497a27720d7ef1a15c8733275340b110a9c7789b1b22b" dependencies = [ "blake3", "bytecheck", "const-decoder", + "constant_time_eq 0.3.1", "crumbles", "dusk-merkle", "dusk-wasmtime", @@ -1763,9 +1846,9 @@ dependencies = [ [[package]] name = "piecrust-uplink" -version = "0.19.0-rc.0" +version = "0.19.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49671c8d75a8c55fc4aff276729ac2e653596e74934fe7c72b592b6b868ad897" +checksum = "97ff1da9d532178dc05a9b07f9457a1138a5f0b4253ea062a138a20cf3907ed7" dependencies = [ "bytecheck", "dlmalloc", @@ -1775,9 +1858,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -1836,28 +1919,39 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.23" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ + "ar_archive_writer", "cc", ] @@ -1883,18 +1977,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -1904,9 +1998,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -1929,7 +2023,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] @@ -1940,9 +2034,9 @@ checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1950,9 +2044,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", @@ -1964,7 +2058,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror", ] @@ -1984,9 +2078,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", @@ -1996,9 +2090,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2007,15 +2101,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" -version = "0.3.6" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] @@ -2028,23 +2122,27 @@ checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" [[package]] name = "rkyv" -version = "0.7.39" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ + "bitvec", "bytecheck", + "bytes", "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid", ] [[package]] name = "rkyv_derive" -version = "0.7.39" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -2107,20 +2205,20 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -2129,12 +2227,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -2152,9 +2244,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2183,35 +2275,36 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2220,9 +2313,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest", "keccak", @@ -2240,6 +2333,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slice-group-by" version = "0.3.1" @@ -2298,9 +2397,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2321,15 +2420,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", - "windows-sys", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -2368,7 +2467,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -2381,6 +2480,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "toml" version = "0.7.8" @@ -2395,9 +2509,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] @@ -2417,9 +2531,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2428,20 +2542,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2463,15 +2577,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -2479,6 +2593,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2489,6 +2609,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2513,18 +2643,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -2535,9 +2674,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2545,26 +2684,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser 0.244.0", +] + [[package]] name = "wasmparser" version = "0.202.0" @@ -2589,6 +2750,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wasmtime-asm-macros" version = "20.0.0" @@ -2606,7 +2779,7 @@ checksum = "7a9f93a3289057b26dc75eb84d6e60d7694f7d169c7c09597495de6e016a13ff" dependencies = [ "cfg-if", "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2636,14 +2809,14 @@ checksum = "8561d9e2920db2a175213d557d71c2ac7695831ab472bbfafb9060cd1034684f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -2671,7 +2844,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2680,6 +2853,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -2689,6 +2868,24 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2767,6 +2964,94 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] [[package]] name = "wyz" @@ -2779,41 +3064,46 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index d57f8fbe..616438b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,19 +14,22 @@ members = [ resolver = "2" [workspace.dependencies] -# Dusk dependencies -dusk-core = "1.4.0" +# Dusk dependencies used by the legacy genesis/test contracts. The standards +# crates pin their Aegis/Forge dependencies in their own manifests so they do +# not force genesis contracts onto a compiler-incompatible dependency graph. +dusk-core = "=1.4.0" dusk-bytes = "0.1.7" -dusk-vm = "1.4.3" -dusk-wallet-core = "1.4.0" -rusk-profile = "1.4.0" -rusk-prover = "1.3.0" +dusk-vm = "=1.4.3" +dusk-wallet-core = "=1.4.0" +rusk-profile = "=1.4.0" +rusk-prover = "=1.3.0" # Other dependencies bytecheck = { version = "0.6.12", default-features = false } criterion = "0.5.1" ff = { version = "0.13", default-features = false } rand = { version = "0.8.5", default-features = false } +parking_lot = "0.12.3" ringbuffer = "0.15" rkyv = { version = "0.7.39", default-features = false } diff --git a/Makefile b/Makefile index c3e7a124..3bbad23c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,13 @@ -SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn +STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter +STANDARDS_TEST_HELPERS := standards/examples/moonlight_call_router +STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES) $(STANDARDS_TEST_HELPERS) +LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn +STANDARDS_PROPTEST_CASES ?= 8192 +STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384 +STANDARDS_DATA_DRIVER_FUZZ_CASES ?= 2048 +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS ?= 4096 -all: setup-compiler $(SUBDIRS) ## Build all the contracts +all: setup-compiler $(LEGACY_SUBDIRS) ## Build the legacy genesis/test contracts help: ## Display this help screen @grep -h \ @@ -8,13 +15,46 @@ help: ## Display this help screen awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' test: wasm ## Run all the tests in the subfolder - $(MAKE) $(SUBDIRS) MAKECMDGOALS=test + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=test wasm: setup-compiler ## Generate the WASM for all the contracts - $(MAKE) $(SUBDIRS) MAKECMDGOALS=wasm + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=wasm + +standards-fmt: ## Check Dusk standards formatting without the Dusk compiler bundle + cargo fmt --manifest-path standards/Cargo.toml --all --check + +standards-check: ## Check the Dusk standards crate without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards check + +standards-test: ## Test the Dusk standards crate without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards test + +standards-wasm: ## Build standards reference contracts without the Dusk compiler bundle + $(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=wasm + +standards-clippy: ## Run standards clippy without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards clippy + $(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=clippy + +standards-ci: standards-fmt standards-check standards-clippy standards-test standards-wasm ## Run regular standards CI checks + +standards-data-drivers: ## Generate Forge data-driver WASM for standards reference contracts + $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm-dd + +standards-properties: ## Run the longer standards property hardening suite + STANDARDS_PROPTEST_CASES=$(STANDARDS_PROPTEST_CASES) \ + STANDARDS_PROPTEST_MAX_SHRINK_ITERS=$(STANDARDS_PROPTEST_MAX_SHRINK_ITERS) \ + cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test properties + +standards-data-driver-fuzz: standards-data-drivers ## Fuzz Forge data-driver JSON/rkyv input codecs + STANDARDS_DATA_DRIVER_FUZZ_CASES=$(STANDARDS_DATA_DRIVER_FUZZ_CASES) \ + STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=$(STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS) \ + cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test data_driver_fuzz -- --ignored + +standards-hardening: standards-properties standards-data-driver-fuzz ## Run long standards hardening checks clippy: setup-compiler ## Run clippy - $(MAKE) $(SUBDIRS) MAKECMDGOALS=clippy + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=clippy keys: ## Create the keys for the circuits ./scripts/download-rusk.sh @@ -24,9 +64,9 @@ COMPILER_VERSION=v0.3.0-rc.1 setup-compiler: ## Setup the Dusk Contract Compiler @./scripts/setup-compiler.sh $(COMPILER_VERSION) -doc: $(SUBDIRS) ## Run doc gen +doc: $(LEGACY_SUBDIRS) ## Run doc gen -$(SUBDIRS): +$(LEGACY_SUBDIRS) $(STANDARDS_WASM_CONTRACTS): $(MAKE) -C $@ $(MAKECMDGOALS) -.PHONY: all test help $(SUBDIRS) +.PHONY: all test help standards-fmt standards-check standards-test standards-wasm standards-clippy standards-ci standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(LEGACY_SUBDIRS) $(STANDARDS_WASM_CONTRACTS) diff --git a/README.md b/README.md index f85cb2c9..318683b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ This repository contains smart contracts for the Dusk ecosystem: - Genesis protocol contracts: part of the genesis state and provide core functionality to the Dusk protocol. +- Contract standards: reusable Dusk-native primitives and examples for access + control, tokens, replay and nonce protection, timelocks, pausing, reentrancy + guards, and upgrade policy. - Test contracts: small contracts used for integration tests and host function coverage. @@ -45,3 +48,15 @@ The on-chain ContractId for the stake contract is: on behalf of a contract via the transfer and stake contracts. - [`host_fn`](tests/host_fn): wraps host functions (hashing, signature/proof verification, chain metadata, etc.) for testing. + +## Contract Standards + +- [`dusk-contract-standards`](standards/dusk-contract-standards): reusable + primitives. +- [`standards/examples`](standards/examples): authorization, DRC20, DRC721, + and proxy-style compositions built from those primitives. + +See [docs/dusk-contract-standards.md](docs/dusk-contract-standards.md) for the +design notes and validation commands, and +[docs/dusk-contract-standards-security.md](docs/dusk-contract-standards-security.md) +for the security model. diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md new file mode 100644 index 00000000..76bdf55a --- /dev/null +++ b/docs/dusk-contract-standards-audit.md @@ -0,0 +1,152 @@ +# Dusk Contract Standards Audit Packet + +This document is the auditor-facing entry point for the Dusk-native standards +layer. It captures the review scope, security assumptions, invariant matrix, +validation commands, and known residual risks for this branch. + +## Scope + +In scope: + +- `standards/dusk-contract-standards/src/**` +- `standards/examples/authorization_counter` +- `standards/examples/drc20_roles_pausable` +- `standards/examples/drc721_collection` +- `standards/examples/multisig_controller` +- `standards/examples/proxy_counter` +- `scripts/dusk-contract-standards-local-smoke.sh` +- `standards/dusk-contract-standards/tests/**` + +Out of scope: + +- Dusk protocol consensus, wallet, node, prover, and VM internals. +- Forge macro implementation correctness, except where the generated + data-driver and Wasm artifacts are exercised through this repository's tests. +- Economic policy choices such as exact upgrade delays, role assignment + procedures, and marketplace royalty enforcement. +- Compatibility with Ethereum/OpenZeppelin ABIs where Dusk-native semantics + deliberately diverge. + +## Security Model + +The standards layer assumes: + +- `CallContext::current()` correctly reports observed Moonlight root callers + through `public_sender`, including normal transfer-contract entrypoint + execution, and reports immediate contract callers for nested calls. +- Phoenix calls do not expose a stable caller identity. Phoenix authorization + must therefore be an explicit Schnorr signature over an `AuthorizedAction`. +- Dusk signature primitives verify according to their upstream implementations. +- `abi::self_id()`, `abi::block_height()`, `abi::emit`, and contract-call + routing are provided correctly by the host. +- Client encoders use the same schema and byte serialization as the generated + Forge data-drivers. + +The layer does not assume: + +- an Ethereum-like `msg.sender` for Phoenix; +- delegatecall-style proxy storage behavior; +- off-chain signatures are safe without chain/contract/domain/action/payload + binding; +- zero principals are valid owners, recipients, spenders, operators, or admins. + +## Invariant Matrix + +| ID | Invariant | Primary Coverage | +| --- | --- | --- | +| AUTH-1 | Signed actions bind chain id, contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke | +| AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke | +| AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke | +| AUTH-4 | Moonlight owners can authorize by observed root caller or signed BLS action; contract owners only by observed immediate contract caller. Transfer-contract entrypoint calls map to `public_sender`, while nested calls do not. | primitives, reference contracts, VM test, `core::context` unit tests | +| MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives | +| MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | +| MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | +| MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects non-owners and duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, properties, VM test | +| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by a 2-of-3 standalone multisig controller through observed inter-contract caller context, while direct owner calls to the governed target fail. | VM test, local-node smoke | +| ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | +| ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | +| PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | +| PAUSE-2 | Approvals remain available while paused and do not move balances or ownership. | VM test, local-node smoke | +| TOKEN20-1 | DRC20 total supply equals the modeled sum of balances after arbitrary operation sequences. | property state-machine model | +| TOKEN20-2 | DRC20 transfer, transfer-from, allowance, mint, burn, cap, and vote checkpoint failures are atomic. | primitives, properties | +| TOKEN721-1 | DRC721 owner, balance, approval, operator, enumerable, and total-supply views remain internally consistent. | property state-machine model | +| TOKEN721-2 | DRC721 mint, transfer, burn, approval, operator, and royalty failures are atomic. | primitives, properties | +| ROYALTY-1 | Royalty receivers cannot be zero, basis points cannot exceed 10,000, and overflow-prone quotes do not mutate state. | primitives, properties | +| NONCE-1 | Nonce domains are independent, monotonic, import/export safe, and atomic on rejection. | primitives, properties | +| REPLAY-1 | Replay keys cannot be reused for the same principal and import/export is monotonic. | primitives, properties | +| PROXY-1 | Upgrade admin rejects zero admin/implementation and binds signed admin actions to exact payloads. | primitives, VM test, local-node smoke | +| PROXY-2 | Prepare, activate, cancel, rollback, and rollback-finalization obey delay/window state transitions and emit events. | primitives, properties, data-driver event decoding | +| TIMELOCK-1 | Timelock scheduling/execution/cancellation follows delay and predecessor constraints. | primitives, properties | +| TIMELOCK-2 | Controller minimum-delay changes are self-governed through scheduled operations, not direct arbitrary admin mutation. | primitives, properties | +| REENTRANCY-1 | Scoped guard rejects nested entry and resets after panics. | primitives | +| ABI-1 | Forge data-driver schemas load for every reference contract. | `tests/data_driver_fuzz.rs` | +| ABI-2 | Function inputs JSON-encode, decode, and re-encode consistently; malformed and mutated payloads do not crash. | `tests/data_driver_fuzz.rs` | +| ABI-3 | Function outputs and typed events decode from rkyv and mutated output/event payloads do not crash readers. | `tests/data_driver_fuzz.rs` | +| NODE-1 | Real local Rusk deployment accepts valid signed calls and rejects replay, bad payload, expired action, pause, and proxy replay cases while preserving expected state. | `scripts/dusk-contract-standards-local-smoke.sh` | + +## Reproducible Validation + +Run the audit-grade validation pass: + +```sh +./scripts/dusk-contract-standards-audit-grade.sh +``` + +Default intensity: + +- `STANDARDS_PROPTEST_CASES=8192` +- `STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384` +- `STANDARDS_DATA_DRIVER_FUZZ_CASES=4096` +- `STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=8192` + +For a funded local-node run, start Rusk and provide wallet/node settings: + +```sh +RUN_LOCAL_NODE_SMOKE=1 \ +RUSK_URL=http://127.0.0.1:18080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +WALLET_DIR=target/rusk-local-smoke/wallet \ +WALLET_RESTORE_FILE=/path/to/wallet.dat \ +./scripts/dusk-contract-standards-audit-grade.sh +``` + +For dependency advisory scanning, install `cargo-audit` and opt in: + +```sh +RUN_CARGO_AUDIT=1 ./scripts/dusk-contract-standards-audit-grade.sh +``` + +## Focused Reviews + +- `docs/dusk-contract-standards-multisig-controller-review.md` + +## Auditor Checklist + +Reviewers should focus on: + +- whether the Dusk principal model is the right abstraction for Moonlight, + Phoenix, and contract callers; +- whether every public signed path uses action-bound helpers before nonce + consumption; +- whether single-owner admin paths should be replaced with + `ThresholdMultisig` composition or standalone `MultisigController` + ownership before production deployment; +- whether payload-hash construction is unambiguous and domain separated enough + for downstream wallets; +- whether pause semantics match product/security expectations; +- whether upgrade and timelock policies are sufficient for production systems; +- whether any event is missing for an indexer-relevant state change; +- whether generated data-driver schemas are acceptable as the client/wallet + ABI source of truth. + +## Residual Risks + +The branch is audit-ready, not audit-complete. Remaining risk areas: + +- independent client encoders may still disagree with Forge data-drivers unless + they are tested differentially; +- Dusk runtime host-function behavior is assumed rather than proven here; +- no formal verification is included for arithmetic or state machines; +- dependency advisory scanning is optional unless `cargo-audit` is installed; +- local-node smoke is deterministic and adversarial for defined invariants, but + it is not a long-running network chaos test. diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md new file mode 100644 index 00000000..5df14a7f --- /dev/null +++ b/docs/dusk-contract-standards-hardening.md @@ -0,0 +1,168 @@ +# Dusk Contract Standards Hardening Track + +This branch is a separate hardening track for the Dusk-native standards layer. +It keeps implementation work close to the contracts, but treats security +validation as a first-class deliverable rather than a final smoke test. + +## Current Scope + +The first hardening pass focuses on invariants that should hold regardless of +the composing contract: + +- signed authorization must bind contract, domain, action id, payload hash, + nonce, principal, and expiry before nonce/replay state is consumed; +- Phoenix signed authorization must preserve nonce and replay-key state across + all rejected envelope, signature, expiry, policy, and replay-key cases; +- owner, role, and upgrade-admin checks must not consume a valid signer nonce + when the signer is not authorized for the policy being checked; +- threshold multisig checks must verify distinct owner quorum before consuming + any signer nonce or replay key; +- mixed owner-set checks must follow the same no-consume-on-unauthorized rule; +- failed token operations must not leave partial native state behind; +- DRC20 total supply must equal the modeled sum of balances for all touched + accounts; +- DRC20 allowance movement must match the reference model exactly; +- DRC721 total supply must equal the modeled number of live token ids; +- DRC721 owner, approval, operator, balance, and enumerable views must remain + internally consistent after arbitrary operation sequences; +- nonce import/export and replay-key import/export must be monotonic, + idempotent, and atomic on rejected nonce consumption; +- voting checkpoints must reject non-monotonic writes without partially + updating account or total-supply checkpoints; +- royalty registry mutations and overflow-prone royalty quotes must leave + registry state unchanged on rejection; +- supply-cap changes must reject cap reductions below current supply and + overflow-prone mints without moving the configured cap; +- owner-set initialization and timelock-controller maintenance operations must + validate before mutating durable state; +- the reserved zero principal must not be accepted as an actor, recipient, + spender, minter, burner, or operator where that would create ownership or + authorization state. + +## Added Validation + +`standards/dusk-contract-standards/tests/properties.rs` adds property-based +state-machine tests for: + +- action-bound authorization rejection cases; +- Phoenix action-bound authorization rejection and replay-key preservation + cases; +- mixed owner sets; +- access-control role/admin mutation; +- timelock scheduling, execution, cancellation, and delay mutation; +- timelock-controller self-governed delay changes and malformed maintenance + payload rejection; +- upgrade-admin prepare, activate, cancel, rollback, and finalization flows; +- nonce/replay state import, consumption, and rejection; +- two-step ownership; +- DRC20 supply caps and voting-unit checkpoints; +- DRC20 accounting and allowance flows; +- DRC721 ownership, approval, operator, balance, and enumerable views; +- DRC721 royalty registry mutation and quote behavior. + +The tests generate random operation sequences, run them against the real +primitive and an independent model, and assert after each operation that: + +- success/failure matches the model; +- successful calls produce the exact expected state; +- failing calls leave the pre-call state unchanged. + +The saved proptest regression file keeps the self-transfer case that caught a +DRC20 accounting bug during this pass. + +The extended invariant pass also found and fixed: + +- a `VotingUnits::move_units` atomicity bug where an account checkpoint could + be written before a later total-supply checkpoint rejected the operation; +- an `OwnerSet::init` atomicity bug where a later invalid owner could leave + earlier owners in the set; +- a `TimelockController::execute_min_delay_change` atomicity bug where a + malformed payload could mark an operation done before being rejected. + +## Commands + +Run the hardening tests with: + +```sh +cargo test -p dusk-contract-standards +cargo test -p dusk-contract-standards --test properties +``` + +Run the long randomized hardening loop with: + +```sh +start=$(date +%s) +end=$((start + 14400)) +while [ "$(date +%s)" -lt "$end" ]; do + PROPTEST_CASES=2048 PROPTEST_MAX_SHRINK_ITERS=8192 \ + cargo test -p dusk-contract-standards --test properties +done +``` + +On April 26, 2026, this loop ran for 14,404 seconds and completed 367 full +property-suite iterations without a failure. + +On April 26, 2026, the extended 16-test property suite also passed a +4,096-case focused run: + +```sh +PROPTEST_CASES=4096 PROPTEST_MAX_SHRINK_ITERS=8192 \ + cargo test -p dusk-contract-standards --test properties +``` + +The property suite now also honors `STANDARDS_PROPTEST_CASES` and +`STANDARDS_PROPTEST_MAX_SHRINK_ITERS`, which lets CI run longer without +editing the test source: + +```sh +STANDARDS_PROPTEST_CASES=8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384 \ + cargo test -p dusk-contract-standards --test properties +``` + +Forge data-driver ABI fuzzing covers JSON-to-rkyv input encoding, input +decoding, roundtrips, malformed JSON, bad shapes, unknown functions, output +decoding, typed event decoding, and mutated input/output/event payloads for the +five standards reference contracts: + +```sh +make standards-data-drivers +STANDARDS_DATA_DRIVER_FUZZ_CASES=2048 \ + cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored +``` + +The `standards-hardening` workflow runs these longer property and data-driver +fuzz jobs on demand and nightly. + +Run the audit-grade validation script with: + +```sh +./scripts/dusk-contract-standards-audit-grade.sh +``` + +See `docs/dusk-contract-standards-audit.md` for the auditor-facing scope, +threat model, invariant matrix, and residual-risk register. + +Run the full standards validation pass with: + +```sh +cargo fmt +cargo test -p dusk-contract-standards +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract +cargo test -p dusk-contract-standards --test examples_vm -- --ignored +make standards-data-drivers +cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored +cargo clippy -p dusk-contract-standards --all-targets -- -D warnings +``` + +## Next Research Items + +The next hardening layer should add differential tests against independent +client encoders, formal or semi-formal arithmetic/state-machine proofs, and +local-node scenario tests that intentionally mix successful transactions with +rejected transactions across block boundaries over longer runs. diff --git a/docs/dusk-contract-standards-multisig-controller-review.md b/docs/dusk-contract-standards-multisig-controller-review.md new file mode 100644 index 00000000..3059a722 --- /dev/null +++ b/docs/dusk-contract-standards-multisig-controller-review.md @@ -0,0 +1,113 @@ +# Multisig Controller Security Review + +Review date: 2026-04-27 + +## Scope + +Reviewed code and validation: + +- `standards/dusk-contract-standards/src/governance/multisig_controller.rs` +- `standards/examples/multisig_controller` +- `standards/examples/proxy_counter` +- multisig primitive, property, VM, local-node smoke, and data-driver tests under + `standards/dusk-contract-standards/tests` + +The review focused on the standalone controller pattern where a target contract +is owned by `Principal::Contract(multisig_controller_id)`, and individual +Moonlight/Phoenix/contract owners approve controller operations. + +## Intended Security Properties + +- The target contract must treat the multisig controller contract as the owner. + Individual multisig owners are not target admins by themselves. +- A proposal with one owner approval must not execute when the threshold is + two. +- Confirmation requires a distinct current owner. Duplicate confirmations, + non-owner confirmations, wrong payload signatures, and replayed signatures + must fail. +- Signed approvals must bind the controller id, nonce domain, action id, + operation id or maintenance payload, signer principal, nonce, expiry, and + payload hash before nonce/replay state is consumed. +- Operation ids must be scoped to chain id, controller contract id, target call + bytes, and salt. +- A ready operation must be removed from pending proposals and tombstoned before + the wrapper attempts target execution. +- Expired proposals are pruned and cannot become ready. +- Authority changes that remove owners or change threshold clear stale pending + operations. + +## Review Results + +No critical or high-severity issues are open from this pass. + +The VM test and local-node smoke deploy `multisig_controller` with three +Phoenix owners and threshold `2`, then deploy `proxy_counter` with +`Principal::Contract(multisig_controller_id)` as admin. They verify that: + +- direct root `proxy_counter.set_value` without authorization fails; +- a direct signed proxy-admin call from a multisig owner fails because the + target owner is the controller contract, not the human owner; +- a non-owner cannot propose and does not advance nonce state; +- one valid owner proposal leaves the operation pending and does not update the + target; +- duplicate confirmation and wrong-payload confirmation fail without nonce + movement; +- a second distinct owner confirms and executes the target call; +- replaying the used confirmation fails; +- a third owner can be the second signer for a later operation, proving the + threshold is truly 2-of-3 rather than a fixed owner pair. + +The native property test exercises the raw controller state machine across +random owners, ids, targets, TTLs, tombstone windows, and timings. It checks +atomicity for non-owner proposals, non-owner confirmations, duplicate +confirmations, one-of-three pending behavior, expiry pruning, ready-state +tombstoning, tombstone replay rejection, and successful re-use only after the +tombstone has expired. + +## Security Notes + +The controller deliberately has no Ethereum-like `msg.sender` assumption for +Phoenix. Phoenix owners approve by Schnorr-signed `AuthorizedAction`. +Moonlight and contract owners may be accepted through observed runtime context +when the host exposes it. + +The Forge wrapper verifies signatures before consuming nonce state, but only +consumes after the controller accepts the proposal/confirmation/maintenance +operation. This avoids burning a valid signature on duplicate confirmations, +wrong threshold membership, invalid targets, expired proposals, or invalid +maintenance payloads. + +Execution is automatic at threshold. There is no separate `execute` step. This +is simpler and avoids a ready-operation queue, but systems that require a final +execution window, keeper role, or time delay should compose the multisig with a +timelock target. + +A target execution failure emits `multisig/operation_executed` with +`success = false`, removes the proposal, and tombstones the id. Retrying the +same logical call requires a new salt. This avoids accidental replay, but +clients must surface failed execution clearly. + +`propose` can count as a confirmation for an existing operation with the same +operation id. This is acceptable because the signed payload is the exact +operation id, but client UX should prefer the explicit `confirm` method after a +proposal already exists. + +## Residual Risks + +- Host-function correctness is assumed. The contract-owner path depends on + `abi::caller()` exposing the immediate caller contract during nested calls. +- Data-driver/ABI correctness is fuzzed for generated schemas, but independent + clients should be differentially tested against those schemas. +- The controller does not solve key-management policy. Owner selection, + hardware-key usage, emergency procedures, and threshold choice remain + deployment responsibilities. +- No formal verification has been performed for the controller state machine. +- The deterministic local-node smoke covers this exact multisig flow, but it is + not a long-running adversarial network campaign. + +## Recommendation + +Use `MultisigController` as the default owner/admin principal for production +references that would otherwise use a single owner. For high-value upgrade or +treasury flows, set the target owner to a timelock controlled by the multisig, +or make the multisig operate only timelock-scheduled actions. diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md new file mode 100644 index 00000000..33835028 --- /dev/null +++ b/docs/dusk-contract-standards-security.md @@ -0,0 +1,104 @@ +# Dusk Contract Standards Security Notes + +## Replay Domains + +Every signed action must bind `chain_id`, `contract`, `domain`, `action_id`, +`nonce`, `principal`, and `payload_hash`. Chain binding prevents an unused +authorization for one network from being replayed on another network with the +same contract id and nonce state. Use separate nonce domains for unrelated +flows: token approvals, role admin, proxy upgrades, voting, and application +actions should not share a nonce stream unless that coupling is deliberate. + +## Expiry and Nonces + +Nonces stop replay after use. Expiry limits how long an unused signature can +float around in clients, relayers, or mempools. Reference client examples use +finite expiry. Use `expires_at = 0` only for offline workflows where a +permanently valid unused signature is acceptable. + +## Phoenix Authorization + +Phoenix does not provide a clean runtime caller identity. Treat Phoenix admin +or owner actions as explicit Schnorr authorizations over an `AuthorizedAction`. +This proves control of the Phoenix signing key represented by the principal; it +does not prove that a specific Phoenix note exists or was spent. + +## Moonlight and Contract Callers + +Moonlight root calls and inter-contract calls can use `CallContext::current()`. +A normal Moonlight transaction reaches the target through the transfer +contract. The standards layer treats that transfer-contract wrapper as the +transaction entrypoint and maps it to `public_sender` only for the root target +call. Nested calls are not rewritten to the public sender; they stay bound to +the immediate caller contract. Signed Moonlight authorization is still useful +for relayed calls or workflows where the submitter is not the owner. Contract +principals should be accepted only from observed inter-contract caller context. + +## Payload Binding + +Do not verify a signature without first checking that the action envelope +matches the call being submitted. Public methods should prefer the action-bound +helpers (`authorize_signed_action`, `authorize_owner_action`, +`authorize_role_action`, and `authorize_admin_action`) so wrong contract, wrong +domain, wrong action id, and wrong payload hash are rejected before nonce state +is consumed. + +Payload hashes must include enough typed structure to prevent ambiguous +concatenation. Reference contracts prefix each payload with an operation tag and +length-prefix variable-size principal encodings before hashing. New standards +should follow that pattern or use a canonical structured encoder. + +## Pausable References + +The reference pausable DRC20 and DRC721 contracts pause all balance-changing +operations: transfers, minting, and burning. Approvals remain available while +paused so accounts can prepare permissions without moving balances or ownership. +Pause and unpause operations emit typed events, and role and royalty policy +changes are also observable through typed events. +Signed DRC721 token/operator approvals follow the same nonce-bound action +model as DRC20 signed approvals and remain available while paused. + +## Reentrancy and Call Stack + +Use `ReentrancyGuard` around state-changing flows that perform external calls +or may later grow such calls. Dusk call-stack behavior is not EVM delegatecall; +avoid importing EVM proxy or reentrancy assumptions directly. + +## Upgrades and Timelocks + +Upgrade admin signatures should bind the exact target implementation and +migration payload. For production systems, put upgrade preparation or +activation behind a timelock, keep a rollback window, and emit lifecycle events +for indexers and wallets. Timelock controller maintenance, including minimum +delay changes, should go through the timelock itself rather than a direct admin +call. + +## Threshold Multisig + +Do not default production admin paths to a single owner when compromise of that +owner would give full control. `ThresholdMultisig` lets composing contracts +require a distinct M-of-N owner quorum over Dusk principals. It counts observed +Moonlight/contract callers when the runtime exposes them, requires Phoenix +owners to use signed approvals, rejects duplicate signers, and verifies all +approvals before consuming any nonce/replay state. + +Multisig approvals should be bound to the exact operation through +`ActionEnvelope`. Owner-set changes, threshold changes, proxy upgrades, token +minting, pausing, and role administration should use separate domains or action +ids so approvals cannot be replayed across policy surfaces. + +Use the standalone `MultisigController` when a ported contract wants a single +owner/admin principal like an Ethereum contract owned by a Safe. The target +contract should assign ownership or the relevant role to the controller's +contract id. Owners then approve the target call through proposal/confirmation +or signed actions, and the controller performs the call as the observed +contract caller after threshold. Operation ids must bind chain id, controller +id, target call bytes, and salt; failed target execution is observable through +the execution event and should be retried with a new salt only when operators +intend a new attempt. + +## Audit Packet + +`docs/dusk-contract-standards-audit.md` is the auditor-facing packet for this +branch. It lists the exact scope, trust assumptions, invariant matrix, +validation commands, and residual risks. diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md new file mode 100644 index 00000000..3c4d2b22 --- /dev/null +++ b/docs/dusk-contract-standards.md @@ -0,0 +1,315 @@ +# Dusk Contract Standards + +This branch adds Dusk-native reusable contract standards rather than a semantic +copy of an EVM library. The goal is to provide secure building blocks that fit +the Dusk execution model, storage model, and client model. + +## Layout + +- `standards/dusk-contract-standards`: reusable primitives, native tests, and + ignored VM tests for the Wasm examples. +- `standards/examples/authorization_counter`: a Forge counter controlled by + replay-protected Moonlight BLS or Phoenix Schnorr signatures. +- `standards/examples/drc20_roles_pausable`: a Forge fungible-token + reference with roles, pause control, cap policy, vote checkpoints, signed + approvals, events, and reentrancy protection. +- `standards/examples/drc721_collection`: a Forge collection composition with + owner control, pause control, approvals, royalties, events, and token + metadata. +- `standards/examples/multisig_controller`: a Forge standalone multisig + controller that can own/administer other contracts through a contract + principal, with proposals, confirmations, tombstones, typed events, and + Dusk-native signed owner approvals. +- `standards/examples/proxy_counter`: a Forge upgrade-admin and state-store + example showing how proxy-like upgrade policy can be modeled without + pretending Dusk has EVM delegatecall semantics. + +## Design Notes + +Dusk identity is not a single address type. The shared `Principal` abstraction +distinguishes Moonlight public accounts, Phoenix authorization identities, and +contracts. `OwnerSet` lets a composing contract accept a mixed ownership policy +instead of forcing everything into an Ethereum-style address. Contracts can use +`CallContext::current()` for runtime Moonlight and inter-contract calls, while +Phoenix flows should pass an explicit authorization identity plus a nonce or +replay key when the application needs that model. +For Moonlight transactions routed through the transfer contract, the standards +layer treats the root transfer-contract call into the target as the transaction +entrypoint and resolves it to the runtime `public_sender`. Nested calls remain +bound to the immediate caller contract. + +`NonceManager` is intentionally domain-separated by `(principal, domain)`. This +lets one contract keep independent monotonic streams for permits, Phoenix +signature authorizations, voting, role delegation, or upgrade approvals. + +`AuthorizationManager` builds on those nonces. Moonlight authorization verifies +BLS signatures over stable action bytes and consumes the matching +principal/domain nonce. Phoenix authorization verifies a Schnorr signature from +the Phoenix public key represented by the principal, then consumes the nonce and +optional replay key. This proves control of the Phoenix signing key without +pretending that Phoenix exposes an address-like runtime caller. + +`SignedAuthorization` is the ABI-facing wrapper for explicit authorizations, +`ActionEnvelope` is the expected chain/contract/domain/action/payload binding, and +`Authorizer` is the reusable per-call adapter. `Ownable`, `OwnerSet`, +`AccessControl`, and `UpgradeAdmin` expose action-bound helper methods that +accept runtime Moonlight/contract callers when available and otherwise verify +the exact signed envelope before consuming nonce/replay state. This keeps +Phoenix out of the `msg.sender` model while still giving contracts one ergonomic +admin path. + +The token modules keep core accounting in reusable Rust state machines. Access +control, pausing, and event emission live in the composing contracts, which is a +better fit for Dusk because call context, host functions, and exposed ABI shape +are contract-specific. + +The examples are Forge contracts. Their ABI argument structs live at crate scope +for schema generation, while the state and exported methods live inside a +`#[dusk_forge::contract]` module. Forge generates the schema, contract `STATE`, +and exported wrappers; the examples do not hand-roll `#[no_mangle]` ABI +functions. + +DRC20 includes optional supply-cap, burnable, pausable, signed approval, and +voting-unit checkpoint patterns. DRC721 includes enumerable queries, burnable, +pausable, signed token/operator approvals, and default/token-specific royalty +patterns. The reference pausable tokens pause all balance-changing operations: +transfers, minting, and burning. Approvals remain available while paused. These +remain composing contract choices rather than mandatory behavior baked into +every token. Reference contracts emit typed pause/unpause, role grant/revoke, +royalty change, transfer, approval, and proxy value/upgrade events instead of +leaving policy mutations silent. + +Proxy support is expressed as an upgrade admin state machine and a namespaced +state store. The example records the active implementation id, delay, migration +payload, rollback window, and emits upgrade lifecycle events. It deliberately +does not expose an EVM-compatible proxy ABI. + +`Timelock` is a standalone scheduling primitive for upgrade/governance flows, +and `TimelockController` composes it with Dusk-native roles for proposer, +executor, canceller, and admin policies. Controller maintenance that changes +the minimum delay is self-governed: it must be scheduled and executed through +the timelock path rather than performed directly by an administrator. + +`ThresholdMultisig` is the Dusk-native answer to single-owner admin risk. It +counts distinct Moonlight, Phoenix, and contract principals toward a threshold: +Moonlight and contract owners may be observed through call context, while +Phoenix owners must approve with signed actions. The primitive verifies every +approval against the exact `ActionEnvelope` and checks distinct owner quorum +before consuming any nonce or replay state. This lets proxy admins, token +admins, pausers, and future governance flows require M-of-N approval instead of +trusting one hot wallet. + +`MultisigController` is the standalone counterpart for DuskEVM/OZ-style ports +where a contract expects one owner/admin principal. The multisig contract is +assigned as that owner, owners propose and confirm a target `ContractCall`, and +the controller performs the call after threshold. Operation ids are bound to +chain id, controller id, target call bytes, and salt; executed operations are +tombstoned to avoid accidental replay. Failed target execution emits a typed +execution event and consumes the proposal, so clients should use a new salt for +an intentional retry. + +## Client Signing Flow + +Clients sign an `AuthorizedAction` for the exact contract action they want to +execute. The action includes: + +- `chain_id`: the Dusk chain id returned by the target runtime; +- `contract`: the target `ContractId`; +- `domain`: a 32-byte nonce stream chosen by the contract; +- `action_id`: a 32-byte id for the exported operation; +- `nonce`: the current nonce for `(principal, domain)`; +- `expires_at`: a contract-defined block/time deadline, or zero for an explicit + permanent unused signature; +- `principal`: the Moonlight or Phoenix principal being authorized; +- `payload_hash`: a contract-defined hash/commitment of the call payload. + +The composing contract defines `payload_hash`. The authorization counter hashes +the `u64` amount with `abi::keccak256(amount.to_be_bytes())`, then checks that +the signed action payload matches the submitted amount before verifying the +signature. Other contracts should use the same pattern: hash only the payload +fields that the signed action is meant to bind, and reject the call before +signature verification if the submitted payload does not match. + +The DRC20 reference exposes `approve_by_authorization`. Its payload hash is: + +1. the ASCII domain tag `drc20.approve`; +2. owner principal length and bytes from `Principal::to_bytes()`; +3. spender principal length and bytes; +4. the big-endian `u64` amount; +5. `keccak256` over the concatenated bytes. + +Admin references follow the same rule. Signed mint, role, royalty, and proxy +calls validate `chain_id`, `contract`, `domain`, `action_id`, and +`payload_hash` before consuming the signature nonce. + +The DRC721 reference exposes `approve_by_authorization` and +`set_approval_for_all_by_authorization`. Token approval hashes use the ASCII +domain tag `drc721.approve`, owner principal bytes, approved principal bytes, +and the big-endian token id. Operator approval hashes use +`drc721.approval_for_all`, owner principal bytes, operator principal bytes, and +a single `0` or `1` approval byte. + +Use stable constants for `domain` and `action_id`. `domain` separates nonce +streams such as permits, role-admin actions, upgrade approvals, and voting. +`action_id` separates operations inside the same stream. Reusing a domain is +fine only when a single monotonic nonce stream is intentional. + +Moonlight signs `AuthorizedAction::message_bytes()` with BLS and submits +`SignedAuthorization::Moonlight`. Phoenix signs +`AuthorizedAction::message_hash()` with Schnorr and submits +`SignedAuthorization::Phoenix`. Public contract methods should use the +action-bound helpers such as `authorize_signed_action`, +`authorize_owner_action`, `authorize_role_action`, or `authorize_admin_action` +so the envelope is checked before nonce state is consumed. The Phoenix +signature proves control of the Phoenix signing key represented by +`Principal::Phoenix`; it does not prove that a specific note exists or was +spent. + +See +`standards/dusk-contract-standards/examples/build_signed_authorizations.rs` for +a compact client-side Rust example that builds the DRC20 signed-approval +and DRC721 signed-approval payload hashes, constructs `AuthorizedAction`, and +signs them with Moonlight BLS and Phoenix Schnorr keys. + +Nonce protection rejects replay after a signed action is consumed. Expiry is +optional but recommended for relayed or delayed execution, because an unused +signature with only a nonce can remain valid until that nonce is consumed by +some other action. The signing example uses a finite expiry by default; +`expires_at = 0` remains available for explicit offline workflows that need a +permanent unused authorization. + +## Forge Data-Drivers + +Each reference contract now has two Forge WASM targets: + +- contract WASM: on-chain, built with the `contract` feature and + `-Z build-std=core,alloc`; +- data-driver WASM: off-chain JSON/rkyv conversion, built with + `data-driver-js` into `target/data-driver`. + +Build all reference data-drivers with: + +```sh +CARGO_TARGET_DIR=target/data-driver \ +cargo build --release --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,multisig-controller/data-driver-js,proxy-counter/data-driver-js +``` + +The generated data-drivers export `init`, `get_schema`, `encode_input_fn`, +`decode_output_fn`, and `decode_event`. The example Makefiles expose this as +`make wasm-dd`, and the top-level `make standards-data-drivers` builds all five. + +## Validation + +The native test suite covers positive and negative paths for: + +- ownership and two-step ownership; +- mixed Moonlight/Phoenix/contract owner sets; +- role grants and revokes; +- replay protection; +- domain-separated nonces; +- Moonlight BLS authorization; +- Phoenix Schnorr authorization with replay keys; +- action-bound authorization helpers that reject wrong envelopes before nonce + movement; +- observed-or-signed owner, role, and upgrade-admin authorization; +- threshold multisig authorization, duplicate signer rejection, observed + Moonlight/contract approval, Phoenix signed approval, and threshold-gated + owner/threshold maintenance; +- standalone multisig controller proposal, confirmation, duplicate-confirmation + rejection, non-owner rejection, expiry cleanup, cancellation, tombstoning, + authority updates, 2-of-3 property coverage, and proxy-as-owner VM execution; +- timelock scheduling, cancellation, execution, and invalid states; +- role-gated timelock controller flows, including self-governed delay updates; +- reentrancy guard behavior; +- pausing; +- DRC20 transfer, allowance changes, mint, burn, initialization, and failure + cases; +- DRC20 supply cap and voting-unit checkpoints; +- DRC721 approval, enumeration, operator transfer, mint, burn, initialization, + and failure cases; +- DRC721 royalty policy; +- upgrade preparation, activation delay, cancellation, rollback, finalization, + events, and namespaced proxy state. + +The hardening branch also adds property-based state-machine tests for token, +authorization, ownership, role, timelock, proxy, nonce/replay, checkpoint, +royalty, multisig-controller, and cap primitives. They compare arbitrary +operation sequences against independent models and assert that rejected +operations leave native state unchanged. See +`docs/dusk-contract-standards-hardening.md` for the current hardening track. + +The ignored VM test deploys all five Wasm examples, checks positive query +paths, performs real Moonlight and Phoenix signed calls against the +authorization counter, covers replay/wrong-payload/wrong-signature/wrong-target +failures, submits signed DRC20 mint and signed DRC20 approvals, verifies paused +DRC20/DRC721 signed mint rejection without nonce movement, submits signed +DRC721 owner actions and signed DRC721 approvals, submits a signed proxy admin +call, executes a proxy admin call through the standalone multisig controller, +covers replay/wrong-payload failures for those reference flows, rejects direct +target calls from individual multisig owners, and calls privileged functions +without a runtime caller to validate the negative authorization path at the VM +boundary. + +Run the focused suite with: + +```sh +cargo test -p dusk-contract-standards +``` + +Build the example Wasm contracts with: + +```sh +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract +``` + +After the Wasm build, run the VM deployment/query test with: + +```sh +cargo test -p dusk-contract-standards --test examples_vm -- --ignored +``` + +Build the Forge data-driver WASM with: + +```sh +make standards-data-drivers +``` + +For a local-node smoke deployment against a running Rusk endpoint: + +```sh +RUSK_URL=http://localhost:8080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +./scripts/dusk-contract-standards-local-smoke.sh +``` + +The script first runs the native suite, builds Wasm, runs the VM deployment +test, serializes deploy-time init arguments using the Dusk ABI serializer, +deploys the five example contracts plus a second proxy owned by the multisig +controller, and queries deployed state. It submits positive and negative +signed transactions, including the 2-of-3 multisig-owned proxy flow, and +asserts nonce/value state after rejected calls. `WALLET_DIR` should point at a +funded local wallet profile. + +For an isolated local smoke wallet, set `WALLET_RESTORE_FILE` to a funded +wallet backup. The script restores it into `WALLET_DIR` when the directory does +not already contain `wallet.keystore.json`: + +```sh +RUSK_URL=http://127.0.0.1:18080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +WALLET_DIR=target/rusk-local-smoke/wallet \ +WALLET_RESTORE_FILE=/path/to/wallet.dat \ +./scripts/dusk-contract-standards-local-smoke.sh +``` diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bc770a12..92da0998 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2024-07-30" +channel = "nightly-2026-02-27" components = ["rust-src", "rustfmt", "cargo", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/dusk-contract-standards-audit-grade.sh b/scripts/dusk-contract-standards-audit-grade.sh new file mode 100755 index 00000000..d422caa9 --- /dev/null +++ b/scripts/dusk-contract-standards-audit-grade.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +STANDARDS_PROPTEST_CASES="${STANDARDS_PROPTEST_CASES:-8192}" +STANDARDS_PROPTEST_MAX_SHRINK_ITERS="${STANDARDS_PROPTEST_MAX_SHRINK_ITERS:-16384}" +STANDARDS_DATA_DRIVER_FUZZ_CASES="${STANDARDS_DATA_DRIVER_FUZZ_CASES:-4096}" +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS="${STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS:-8192}" +RUN_LOCAL_NODE_SMOKE="${RUN_LOCAL_NODE_SMOKE:-0}" +RUN_CARGO_AUDIT="${RUN_CARGO_AUDIT:-0}" + +cd "$ROOT_DIR" + +echo "Checking formatting" +cargo fmt --check +cargo fmt --manifest-path standards/Cargo.toml --all --check + +echo "Running standards clippy" +cargo clippy --manifest-path standards/Cargo.toml -p dusk-contract-standards --all-targets -- -D warnings + +echo "Running native standards tests" +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards + +echo "Building reference contract Wasm" +cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p moonlight-call-router \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,moonlight-call-router/contract,multisig-controller/contract,proxy-counter/contract + +echo "Running VM reference deployment tests" +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test examples_vm -- --ignored + +echo "Running long property and data-driver hardening" +STANDARDS_PROPTEST_CASES="$STANDARDS_PROPTEST_CASES" \ +STANDARDS_PROPTEST_MAX_SHRINK_ITERS="$STANDARDS_PROPTEST_MAX_SHRINK_ITERS" \ +STANDARDS_DATA_DRIVER_FUZZ_CASES="$STANDARDS_DATA_DRIVER_FUZZ_CASES" \ +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS="$STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS" \ +make standards-hardening + +if [[ "$RUN_CARGO_AUDIT" == "1" ]]; then + echo "Running cargo audit" + cargo audit +else + echo "Skipping cargo audit; set RUN_CARGO_AUDIT=1 when cargo-audit is installed" +fi + +if [[ "$RUN_LOCAL_NODE_SMOKE" == "1" ]]; then + echo "Running funded local-node smoke" + ./scripts/dusk-contract-standards-local-smoke.sh +else + echo "Skipping funded local-node smoke; set RUN_LOCAL_NODE_SMOKE=1 with RUSK_URL/RUSK_WALLET_BIN/WALLET_DIR" +fi + +echo "Audit-grade standards validation completed" diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh new file mode 100755 index 00000000..59362e19 --- /dev/null +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -0,0 +1,609 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUSK_URL="${RUSK_URL:-http://localhost:8080}" +RUSK_WALLET_BIN="${RUSK_WALLET_BIN:-$(command -v rusk-wallet || true)}" +WALLET_DIR="${WALLET_DIR:-${ROOT_DIR}/target/dusk-contract-standards-wallet}" +WALLET_PASSWORD="${WALLET_PASSWORD:-password}" +WALLET_RESTORE_FILE="${WALLET_RESTORE_FILE:-}" +DEPLOY_NONCE_BASE="${DEPLOY_NONCE_BASE:-100}" + +cd "$ROOT_DIR" + +echo "Running standards unit tests" +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards + +echo "Building example contracts" +cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p moonlight-call-router \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,moonlight-call-router/contract,multisig-controller/contract,proxy-counter/contract + +echo "Building Forge data-drivers" +CARGO_TARGET_DIR="${ROOT_DIR}/target/data-driver" \ +cargo build --manifest-path standards/Cargo.toml --release --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p multisig-controller \ + -p proxy-counter \ + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,multisig-controller/data-driver-js,proxy-counter/data-driver-js + +echo "Running VM example deployment tests" +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test examples_vm -- --ignored + +if [[ -z "$RUSK_WALLET_BIN" ]]; then + cat >&2 <<'MSG' +rusk-wallet was not found. + +Set RUSK_WALLET_BIN=/path/to/rusk-wallet and RUSK_URL=http://host:port for a running local node, +then rerun this script to deploy the standards examples. +MSG + exit 2 +fi + +if ! curl -fsS \ + -H 'Content-Type: text/plain' \ + --data '{ block(height: -1) { header { height } } }' \ + "${RUSK_URL}/on/graphql/query" >/dev/null; then + cat >&2 </dev/null 2>&1; then + echo "xxd was not found; it is required to submit binary query arguments." >&2 + exit 2 +fi + +if [[ -z "${DUSK_CHAIN_ID:-}" ]]; then + chain_info="$(curl -fsS "${RUSK_URL}/on/node/info" 2>/dev/null || true)" + DUSK_CHAIN_ID="$( + printf '%s' "$chain_info" | + sed -n 's/.*"chain_id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | + head -n1 + )" + if [[ -z "$DUSK_CHAIN_ID" ]]; then + DUSK_CHAIN_ID=250 + echo "Could not read chain id from ${RUSK_URL}; defaulting DUSK_CHAIN_ID=${DUSK_CHAIN_ID}" >&2 + fi +fi +export DUSK_CHAIN_ID +echo "Using DUSK_CHAIN_ID=${DUSK_CHAIN_ID}" + +mkdir -p "$WALLET_DIR" + +if [[ -n "$WALLET_RESTORE_FILE" && ! -f "$WALLET_DIR/wallet.keystore.json" ]]; then + if [[ ! -f "$WALLET_RESTORE_FILE" ]]; then + echo "WALLET_RESTORE_FILE does not exist: ${WALLET_RESTORE_FILE}" >&2 + exit 2 + fi + + echo "Restoring local smoke wallet from ${WALLET_RESTORE_FILE}" + RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + restore -f "$WALLET_RESTORE_FILE" +fi + +encode_args() { + cargo run -q --manifest-path standards/Cargo.toml -p dusk-contract-standards --example encode_local_smoke_args -- "$@" +} + +deploy_contract() { + local name="$1" + local wasm="$2" + local nonce="$3" + local init_args="$4" + local output + local contract_id + local status + + echo "Deploying ${name}" >&2 + set +e + output="$(RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + contract-deploy \ + --code "$wasm" \ + --deploy-nonce "$nonce" \ + --init-args "$init_args" 2>&1)" + status=$? + set -e + printf '%s\n' "$output" >&2 + if [[ "$status" -ne 0 ]]; then + echo "Deployment failed for ${name}" >&2 + exit "$status" + fi + + contract_id="$( + printf '%s\n' "$output" | + awk ' + /Deploying/ { + for (i = 1; i <= NF; i++) { + token = $i + gsub(/[^0-9a-fA-F]/, "", token) + if (length(token) == 64) { + print tolower(token) + exit + } + } + } + ' + )" + + if [[ -z "$contract_id" ]]; then + echo "Unable to parse deployed contract id for ${name}" >&2 + exit 1 + fi + + echo "$contract_id" +} + +query_contract() { + local name="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local tmp_dir + local bytes + + tmp_dir="$(mktemp -d)" + printf '%s' "$args_hex" | xxd -r -p >"${tmp_dir}/args.bin" + curl -fsS \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @"${tmp_dir}/args.bin" \ + "${RUSK_URL}/on/contracts:${contract_id}/${fn_name}" \ + -o "${tmp_dir}/response.bin" + bytes="$(wc -c <"${tmp_dir}/response.bin")" + rm -rf "$tmp_dir" + + if [[ "$bytes" -eq 0 ]]; then + echo "${name}.${fn_name} returned an empty response" >&2 + exit 1 + fi + + echo "Queried ${name}.${fn_name} (${bytes} bytes)" +} + +query_hex() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local tmp_dir + + tmp_dir="$(mktemp -d)" + printf '%s' "$args_hex" | xxd -r -p >"${tmp_dir}/args.bin" + curl -fsS \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @"${tmp_dir}/args.bin" \ + "${RUSK_URL}/on/contracts:${contract_id}/${fn_name}" \ + -o "${tmp_dir}/response.bin" + xxd -p -c 256 "${tmp_dir}/response.bin" + rm -rf "$tmp_dir" +} + +query_u64() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + + printf '%s' "$(query_hex "$contract_id" "$fn_name" "$args_hex")" | + xxd -r -p | + od -An -t u8 | + tr -d ' \n' +} + +query_bool() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local value + + value="$(query_hex "$contract_id" "$fn_name" "$args_hex")" + case "$value" in + 00) echo "false" ;; + 01) echo "true" ;; + *) + echo "${contract_id}.${fn_name} returned invalid bool hex: ${value}" >&2 + exit 1 + ;; + esac +} + +assert_eq() { + local label="$1" + local actual="$2" + local expected="$3" + + if [[ "$actual" != "$expected" ]]; then + echo "Assertion failed for ${label}: got ${actual}, expected ${expected}" >&2 + exit 1 + fi + + echo "Asserted ${label} = ${expected}" +} + +call_contract() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local output_file="$4" + local status + + set +e + RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + contract-call \ + --contract-id "$contract_id" \ + --fn-name "$fn_name" \ + --fn-args "$args_hex" >"$output_file" 2>&1 + status=$? + set -e + + return "$status" +} + +expect_call_ok() { + local label="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local output_file + + output_file="$(mktemp)" + if ! call_contract "$contract_id" "$fn_name" "$args_hex" "$output_file"; then + echo "Expected ${label} to succeed, but wallet command failed" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + if grep -q 'Transaction error:' "$output_file"; then + echo "Expected ${label} to succeed, but transaction reverted" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + rm -f "$output_file" + echo "Accepted ${label}" +} + +expect_call_rejected() { + local label="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local output_file + + output_file="$(mktemp)" + if call_contract "$contract_id" "$fn_name" "$args_hex" "$output_file"; then + if ! grep -q 'Transaction error:' "$output_file"; then + echo "Expected ${label} to be rejected, but it succeeded" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + fi + rm -f "$output_file" + echo "Rejected ${label}" +} + +run_signed_invariants() { + local auth_id="$1" + local drc20_id="$2" + local drc721_id="$3" + local proxy_id="$4" + local expires_at="${SIGNED_EXPIRES_AT:-100000}" + local unit_args + local phoenix_call + local moonlight_call + local proxy_call + + unit_args="$(encode_args unit)" + + assert_eq "authorization counter initial value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 0 + assert_eq "DRC20 initial supply" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1000 + assert_eq "DRC721 initial supply" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 1 + assert_eq "proxy initial value" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + phoenix_call="$(encode_args auth-counter-phoenix "$auth_id" 0 "$expires_at" 21)" + expect_call_ok "Phoenix counter set" "$auth_id" set_value_by_phoenix "$phoenix_call" + assert_eq "Phoenix counter value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 21 + assert_eq "Phoenix counter nonce after success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter replay" "$auth_id" set_value_by_phoenix "$phoenix_call" + assert_eq "Phoenix counter nonce after replay" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter bad payload" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 "$expires_at" 22 bad-payload)" + assert_eq "Phoenix counter nonce after bad payload" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter expired action" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 1 22)" + assert_eq "Phoenix counter nonce after expired action" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_ok "Phoenix counter second set" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 "$expires_at" 22)" + assert_eq "Phoenix counter nonce after second success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 2 + + moonlight_call="$(encode_args auth-counter-moonlight "$auth_id" 0 "$expires_at" 31)" + expect_call_ok "Moonlight counter set" "$auth_id" set_value_by_moonlight "$moonlight_call" + assert_eq "Moonlight counter value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 31 + assert_eq "Moonlight counter nonce after success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_rejected "Moonlight counter replay" "$auth_id" set_value_by_moonlight "$moonlight_call" + assert_eq "Moonlight counter nonce after replay" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_rejected "Moonlight counter wrong action" \ + "$auth_id" set_value_by_moonlight \ + "$(encode_args auth-counter-moonlight "$auth_id" 1 "$expires_at" 32 wrong-action)" + assert_eq "Moonlight counter nonce after wrong action" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_ok "Moonlight counter second set" \ + "$auth_id" set_value_by_moonlight \ + "$(encode_args auth-counter-moonlight "$auth_id" 1 "$expires_at" 32)" + assert_eq "Moonlight counter nonce after second success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 2 + + expect_call_ok "DRC20 signed mint" \ + "$drc20_id" mint "$(encode_args drc20-mint "$drc20_id" 0 "$expires_at" 5)" + assert_eq "DRC20 supply after mint" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1005 + assert_eq "DRC20 admin nonce after mint" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 1 + expect_call_ok "DRC20 signed pause" \ + "$drc20_id" pause "$(encode_args drc20-admin pause "$drc20_id" 1 "$expires_at")" + assert_eq "DRC20 paused" \ + "$(query_bool "$drc20_id" paused "$unit_args")" true + assert_eq "DRC20 admin nonce after pause" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 2 + expect_call_rejected "DRC20 paused mint" \ + "$drc20_id" mint "$(encode_args drc20-mint "$drc20_id" 2 "$expires_at" 6)" + assert_eq "DRC20 supply after paused mint" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1005 + assert_eq "DRC20 admin nonce after paused mint" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 2 + expect_call_rejected "DRC20 paused burn" \ + "$drc20_id" burn "$(encode_args u64 1)" + expect_call_ok "DRC20 signed unpause" \ + "$drc20_id" unpause "$(encode_args drc20-admin unpause "$drc20_id" 2 "$expires_at")" + assert_eq "DRC20 unpaused" \ + "$(query_bool "$drc20_id" paused "$unit_args")" false + assert_eq "DRC20 admin nonce after unpause" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 3 + + expect_call_ok "DRC721 signed mint" \ + "$drc721_id" mint "$(encode_args drc721-mint "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 supply after mint" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 2 + assert_eq "DRC721 admin nonce after mint" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 1 + expect_call_ok "DRC721 signed token approval" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 approval nonce after token approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_rejected "DRC721 signed token approval replay" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 approval nonce after token approval replay" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_rejected "DRC721 signed token approval bad payload" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 1 "$expires_at" 2 bad-payload)" + assert_eq "DRC721 approval nonce after bad payload" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_ok "DRC721 signed operator approval" \ + "$drc721_id" set_approval_for_all_by_authorization \ + "$(encode_args drc721-operator-approval "$drc721_id" 1 "$expires_at" true)" + assert_eq "DRC721 signed operator approval state" \ + "$(query_bool "$drc721_id" is_approved_for_all "$(encode_args drc721-operator-query)")" true + assert_eq "DRC721 approval nonce after operator approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 2 + expect_call_ok "DRC721 signed pause" \ + "$drc721_id" pause "$(encode_args drc721-admin pause "$drc721_id" 1 "$expires_at")" + assert_eq "DRC721 paused" \ + "$(query_bool "$drc721_id" paused "$unit_args")" true + assert_eq "DRC721 admin nonce after pause" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 2 + expect_call_ok "DRC721 signed operator approval while paused" \ + "$drc721_id" set_approval_for_all_by_authorization \ + "$(encode_args drc721-operator-approval "$drc721_id" 2 "$expires_at" false)" + assert_eq "DRC721 signed operator approval state while paused" \ + "$(query_bool "$drc721_id" is_approved_for_all "$(encode_args drc721-operator-query)")" false + assert_eq "DRC721 approval nonce after paused operator approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 3 + expect_call_rejected "DRC721 paused mint" \ + "$drc721_id" mint "$(encode_args drc721-mint "$drc721_id" 2 "$expires_at" 3)" + assert_eq "DRC721 supply after paused mint" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 2 + assert_eq "DRC721 admin nonce after paused mint" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 2 + expect_call_rejected "DRC721 paused burn" \ + "$drc721_id" burn "$(encode_args u64 2)" + expect_call_ok "DRC721 signed unpause" \ + "$drc721_id" unpause "$(encode_args drc721-admin unpause "$drc721_id" 2 "$expires_at")" + assert_eq "DRC721 unpaused" \ + "$(query_bool "$drc721_id" paused "$unit_args")" false + assert_eq "DRC721 admin nonce after unpause" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 3 + + proxy_call="$(encode_args proxy-set "$proxy_id" 0 "$expires_at" 7)" + expect_call_ok "proxy signed set value" "$proxy_id" set_value "$proxy_call" + assert_eq "proxy value after signed set" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 7 + assert_eq "proxy admin nonce after signed set" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 1 + expect_call_rejected "proxy replay" "$proxy_id" set_value "$proxy_call" + assert_eq "proxy value after replay" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 7 + assert_eq "proxy admin nonce after replay" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 1 + + echo "Local signed invariant checks completed" +} + +run_multisig_invariants() { + local multisig_id="$1" + local proxy_id="$2" + local expires_at="${SIGNED_EXPIRES_AT:-100000}" + local unit_args + local target_args + local op_id + local second_target_args + local second_op_id + + unit_args="$(encode_args unit)" + + assert_eq "multisig-governed proxy initial value" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig-governed proxy direct set without auth" \ + "$proxy_id" set_value "$(encode_args proxy-set-no-auth 55)" + assert_eq "multisig-governed proxy value after direct no-auth set" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig-governed proxy direct owner signature" \ + "$proxy_id" set_value "$(encode_args proxy-set "$proxy_id" 0 "$expires_at" 66)" + assert_eq "multisig-governed proxy owner nonce after direct signature" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 0 + assert_eq "multisig-governed proxy value after direct owner signature" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + target_args="$(encode_args multisig-target "$proxy_id" 77 1)" + op_id="$(query_hex "$multisig_id" operation_id "$target_args")" + + expect_call_rejected "multisig non-owner propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 4 0 "$expires_at" 77 1 "$op_id")" + assert_eq "multisig non-owner nonce after rejected propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 4)")" 0 + + expect_call_ok "multisig owner one propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 1 0 "$expires_at" 77 1 "$op_id")" + assert_eq "multisig owner one nonce after propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 1 + assert_eq "multisig-governed proxy value after one of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig duplicate confirmation" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 1 1 "$expires_at" "$op_id")" + assert_eq "multisig owner one nonce after duplicate confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 1 + + expect_call_rejected "multisig wrong payload confirmation" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id" bad-payload)" + assert_eq "multisig owner two nonce after wrong payload" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 0 + + expect_call_ok "multisig owner two confirmation executes proxy" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id")" + assert_eq "multisig-governed proxy value after two of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 77 + assert_eq "multisig owner two nonce after confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 1 + + expect_call_rejected "multisig confirmation replay" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id")" + assert_eq "multisig owner two nonce after replay" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 1 + + second_target_args="$(encode_args multisig-target "$proxy_id" 88 2)" + second_op_id="$(query_hex "$multisig_id" operation_id "$second_target_args")" + expect_call_ok "multisig second operation owner one propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 1 1 "$expires_at" 88 2 "$second_op_id")" + assert_eq "multisig-governed proxy value after second one of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 77 + assert_eq "multisig owner one nonce after second propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 2 + + expect_call_ok "multisig owner three confirmation executes proxy" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 3 0 "$expires_at" "$second_op_id")" + assert_eq "multisig-governed proxy value after owner one plus three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 88 + assert_eq "multisig owner three nonce after confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 3)")" 1 + + echo "Local multisig invariant checks completed" +} + +unit_args="$(encode_args unit)" + +auth_id="$(deploy_contract \ + "authorization counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/authorization_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 0))" \ + "$unit_args")" +query_contract "authorization counter" "$auth_id" "value" "$unit_args" + +drc20_id="$(deploy_contract \ + "DRC20 roles/pausable" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/drc20_roles_pausable.wasm" \ + "$((DEPLOY_NONCE_BASE + 1))" \ + "$(encode_args drc20-init)")" +query_contract "DRC20 roles/pausable" "$drc20_id" "total_supply" "$unit_args" + +drc721_id="$(deploy_contract \ + "DRC721 collection" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/drc721_collection.wasm" \ + "$((DEPLOY_NONCE_BASE + 2))" \ + "$(encode_args drc721-init)")" +query_contract "DRC721 collection" "$drc721_id" "total_supply" "$unit_args" + +multisig_id="$(deploy_contract \ + "multisig controller" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/multisig_controller.wasm" \ + "$((DEPLOY_NONCE_BASE + 3))" \ + "$(encode_args multisig-init)")" +query_contract "multisig controller" "$multisig_id" "threshold" "$unit_args" + +proxy_id="$(deploy_contract \ + "proxy counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/proxy_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 4))" \ + "$(encode_args proxy-init)")" +query_contract "proxy counter" "$proxy_id" "value" "$unit_args" + +multisig_proxy_id="$(deploy_contract \ + "multisig-owned proxy counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/proxy_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 5))" \ + "$(encode_args proxy-init-contract "$multisig_id")")" +query_contract "multisig-owned proxy counter" "$multisig_proxy_id" "value" "$unit_args" + +run_signed_invariants "$auth_id" "$drc20_id" "$drc721_id" "$proxy_id" +run_multisig_invariants "$multisig_id" "$multisig_proxy_id" + +echo "Local standards smoke completed" diff --git a/standards/Cargo.lock b/standards/Cargo.lock new file mode 100644 index 00000000..32a0da6f --- /dev/null +++ b/standards/Cargo.lock @@ -0,0 +1,3116 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3a13b34da09176a8baba701233fdffbaa7c1b1192ce031a3da4e55ce1f1a56" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest 0.10.7", + "sha2 0.10.9", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-groth16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ceafa83848c3e390f1cbf124bc3193b3e639b3f02009e0e290809a501b95fc" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-relations" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00796b6efc05a3f48225e59cb6a2cda78881e7c390872d5786aaf112f31fb4f0" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-snark" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d3cc6833a335bb8a600241889ead68ee89a3cf8448081fb7694c0fe503da63" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "authorization-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls12_381-bls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c61c05b7bfa0cff7b7f6bf000589de5541fc03b702adadfbcfbb6d782a2d643" +dependencies = [ + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "ff", + "rand_core 0.6.4", + "rkyv", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "c-kzg" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16507ccb84a57d44c40cf9b8e6741eb560bc44122b325dcff2fdbfa4959b2110" +dependencies = [ + "arbitrary", + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5241cd7938b1b415942e943ea96f615953d500b50347b505b0b507080bad5a6f" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf72ceaf38f7d41194d0cf6748214d8ef7389167fe09aad80f87646dbfa325b" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee7fde5cd9173f00ce02c491ee9e306d64740f4b1a697946e0474f389999e13" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49bec6a517e78d4067500dc16acb558e772491a2bcb37301127448adfb8413c" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead4ea497b2dc2ac31fcabd6d5d0d5dc25b3964814122e343724bdf65a53c843" + +[[package]] +name = "cranelift-control" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81e8028c8d711ea7592648e70221f2e54acb8665f7ecd49545f021ec14c3341" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32acd0632ba65c2566e75f64af9ef094bb8d90e58a9fbd33d920977a9d85c054" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a395a704934aa944ba8939cac9001174b9ae5236f48bc091f89e33bb968336f6" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b325ce81c4ee7082dc894537eb342c37898e14230fe7c02ea945691db3e2dd01" + +[[package]] +name = "cranelift-native" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea11f5ac85996fa093075d66397922d4f56085d5d84ec13043d0cd4f159c6818" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f175d4e299a8edabfbd64fa93c7650836cc8ad7f4879f9bd2632575a1f12d0" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools 0.12.1", + "log", + "smallvec", + "wasmparser 0.202.0", + "wasmtime-types", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crumbles" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3476b7dda7a457e938e4b958aa7b8db66eb3f6bc72894454a86f99bab3dea2" +dependencies = [ + "libc", + "rangemap", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-hex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6618553c32cd1c1f4fbdb9418cc035f3168422f24406ebb08576f6db5ed6ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "drc20-roles-pausable" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "drc721-collection" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-bls12_381" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633aa835dd9e4db7ad5dc94a3f95d44ff5c6856ec3c83c15eb75366cfb5ee68a" +dependencies = [ + "blake2b_simd", + "bytecheck", + "digest 0.9.0", + "dusk-bytes", + "ff", + "group", + "hex", + "pairing", + "rand_core 0.6.4", + "rkyv", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "dusk-bytes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d209b92f0741edf1d99369bd4c1a1ef2fd0a85e885220f9e3fb0df3c61337f" +dependencies = [ + "derive-hex", +] + +[[package]] +name = "dusk-contract-standards" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "dusk-vm", + "proptest", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "serde_with", + "time", +] + +[[package]] +name = "dusk-core" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7053f16922b6cce10036bda331f5dd373a64ff66972604c2fc3bca594d2158" +dependencies = [ + "ark-bn254", + "ark-groth16", + "ark-relations", + "ark-serialize", + "blake2b_simd", + "bls12_381-bls", + "bytecheck", + "c-kzg", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "ff", + "jubjub-schnorr", + "phoenix-circuits", + "phoenix-core", + "piecrust-uplink", + "poseidon-merkle", + "rand 0.8.6", + "rkyv", + "serde", + "serde_with", + "sha2 0.10.9", +] + +[[package]] +name = "dusk-data-driver" +version = "0.3.2-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191e9b51e34ecb1a22d79de6df7e6e2971121e0069d60d738df9059837f46a1c" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-wasmtime", + "parking_lot", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31edc24ce1a8cfb6ee61d3941145c4b475117c2c0450a6342dc30907c61a5e93" +dependencies = [ + "dusk-forge-contract", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge-contract" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9f361a4eddded990e22c61dece5282257b141bf4d4d8cd2f4fda5a17618791" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dusk-jubjub" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c388e710446aabbe7c7717109a239cea254594253ceb94e9134976c5b59509" +dependencies = [ + "bitvec", + "blake2b_simd", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "ff", + "group", + "hex", + "rand_core 0.6.4", + "rkyv", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "dusk-merkle" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6af2fdeaa2b2e290827e6a421575ac03836213d2ed00952edd9de153581a097" +dependencies = [ + "bytecheck", + "dusk-bytes", + "rkyv", +] + +[[package]] +name = "dusk-plonk" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949ec126023c47910d93a0fcb35ef20243f75eb7a726fcaa6541d8c44fadb66d" +dependencies = [ + "blake2b_simd", + "bytecheck", + "cfg-if", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "ff", + "hashbrown 0.9.1", + "itertools 0.9.0", + "merlin", + "miniz_oxide", + "msgpacker", + "rand_core 0.6.4", + "rkyv", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "dusk-poseidon" +version = "0.42.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb428dc6d7aac399fd2c5929fd6e0b2c7b83392d3eb8c9a7b29a916d0087063d" +dependencies = [ + "dusk-bls12_381", + "dusk-jubjub", + "dusk-plonk", + "dusk-safe", +] + +[[package]] +name = "dusk-safe" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3954d110d0d0f20555048d7171c2e6dfb54fd9a4220d30cc73dc66a88af42d" +dependencies = [ + "zeroize", +] + +[[package]] +name = "dusk-vm" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e78bea4ffcca10eb95f687d7169e94c06e0246dd4023d9f76b213877b2e123" +dependencies = [ + "blake2b_simd", + "blake3", + "bytecheck", + "c-kzg", + "dusk-bytes", + "dusk-core", + "dusk-poseidon", + "lru", + "piecrust", + "rkyv", + "secp256k1", + "serde", + "sha2 0.10.9", + "sha3", + "tracing", + "wasmparser 0.240.0", +] + +[[package]] +name = "dusk-wasmtime" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e44c86778e534ece079bd7cfb8668a9a7591c4703585c6f593258699449f10" +dependencies = [ + "anyhow", + "bincode", + "bumpalo", + "cfg-if", + "dusk-wasmtime-cranelift", + "dusk-wasmtime-environ", + "dusk-wasmtime-runtime", + "gimli", + "indexmap", + "libc", + "log", + "object 0.33.0", + "once_cell", + "paste", + "rayon", + "rustix 0.38.44", + "serde", + "serde_derive", + "serde_json", + "target-lexicon", + "wasmparser 0.202.0", + "wasmtime-jit-icache-coherence", + "wasmtime-slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "dusk-wasmtime-cranelift" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc15917a4c6e33e358f0bfaf2bac0700487f055dab627088fa70e60cef37804e" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "dusk-wasmtime-environ", + "gimli", + "log", + "object 0.33.0", + "target-lexicon", + "thiserror", + "wasmparser 0.202.0", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "dusk-wasmtime-environ" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391fc0ac7b153f80ffddb9e19a02fc3d642d58989448fbbeb44c5d5992e17fc6" +dependencies = [ + "anyhow", + "bincode", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object 0.33.0", + "paste", + "serde", + "serde_derive", + "target-lexicon", + "thiserror", + "wasmparser 0.202.0", + "wasmtime-types", +] + +[[package]] +name = "dusk-wasmtime-runtime" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d033283093a4faa4b8ddc32085ab3825faa1ca8dc6785324c46fdc7760ad627d" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "dusk-wasmtime-environ", + "indexmap", + "libc", + "log", + "mach2", + "memfd", + "memoffset", + "paste", + "psm", + "rustix 0.38.44", + "sptr", + "wasmtime-asm-macros", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jubjub-elgamal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22c81bad2a54e37f6eadcdba35f35761b7dba5665af26ea16f29f0d9372fb64f" +dependencies = [ + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", +] + +[[package]] +name = "jubjub-schnorr" +version = "0.7.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a7208d3397238bd5fce848642b923f7aebed6d58cd472ad72027ba68167167" +dependencies = [ + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "ff", + "rand_core 0.6.4", + "rkyv", + "serde", + "zeroize", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "moonlight-call-router" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-forge", + "rkyv", +] + +[[package]] +name = "msgpacker" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134a72ec3a6d034e9c767344ce8aebde228eef4e1343fff3ea209993f12920b3" +dependencies = [ + "msgpacker-derive", +] + +[[package]] +name = "msgpacker-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c334ab77a6d4f29c5c2e41149c0f03af4082923aa364aa0c9876232f33f9853f" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "multisig-controller" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" +dependencies = [ + "crc32fast", + "hashbrown 0.14.5", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phoenix-circuits" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5583c3db8adc437f7854319a6a815666b95475797ba82d8cb5f9f5a9f18fe55" +dependencies = [ + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "jubjub-elgamal", + "jubjub-schnorr", + "phoenix-core", + "poseidon-merkle", +] + +[[package]] +name = "phoenix-core" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930c6dd9d1054cf0cd725325323a5381fe3854708a2cebe38ccab8699579e2e" +dependencies = [ + "aes-gcm", + "base64", + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-poseidon", + "ff", + "hex", + "hkdf", + "jubjub-elgamal", + "jubjub-schnorr", + "rand 0.8.6", + "rkyv", + "serde", + "serde_with", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "piecrust" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768a69c4aac88337ec2c1b9ac1624d4c52bd50c6ecbdaed70b5854006d56fe01" +dependencies = [ + "blake3", + "bytecheck", + "const-decoder", + "crumbles", + "dusk-merkle", + "dusk-wasmtime", + "hex", + "indexmap", + "memmap2", + "piecrust-uplink", + "rand 0.8.6", + "rkyv", + "tempfile", + "thiserror", + "tracing", +] + +[[package]] +name = "piecrust-uplink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df8d2d3f5deb052f61c65d01d892c4babe604cd884cdd2fcef7f6da4392b476" +dependencies = [ + "bytecheck", + "dlmalloc", + "hex", + "rkyv", + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "poseidon-merkle" +version = "0.9.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e801b94d059823f3dc9d2af14e2a7438863e4b23736e324ebb368991545d52" +dependencies = [ + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-merkle", + "dusk-plonk", + "dusk-poseidon", + "rkyv", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proxy-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regalloc2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +dependencies = [ + "hashbrown 0.13.2", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64", + "chrono", + "hex", + "serde", + "serde_derive", + "serde_json", + "time", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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.117", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmparser" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6998515d3cf3f8b980ef7c11b29a9b1017d4cf86b99ae93b546992df9931413" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.240.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625ee94c72004f3ea0228989c9506596e469517d7d0ed66f7300d1067bdf1ca9" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9f93a3289057b26dc75eb84d6e60d7694f7d169c7c09597495de6e016a13ff" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-slab" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3655075824a374c536a2b2cc9283bb765fcdf3d58b58587862c48571ad81ef" + +[[package]] +name = "wasmtime-types" +version = "20.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5381ff174faded38c7b2085fbe430dff59489c87a91403354d710075750fb" +dependencies = [ + "cranelift-entity", + "serde", + "serde_derive", + "thiserror", + "wasmparser 0.202.0", +] + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8561d9e2920db2a175213d557d71c2ac7695831ab472bbfafb9060cd1034684f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/standards/Cargo.toml b/standards/Cargo.toml new file mode 100644 index 00000000..3101a21a --- /dev/null +++ b/standards/Cargo.toml @@ -0,0 +1,47 @@ +[workspace] +members = [ + "dusk-contract-standards", + "examples/authorization_counter", + "examples/drc20_roles_pausable", + "examples/drc721_collection", + "examples/moonlight_call_router", + "examples/multisig_controller", + "examples/proxy_counter", +] + +resolver = "2" + +[workspace.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = "=0.3.2-alpha.1" +dusk-forge = "=0.3.0" +dusk-vm = "=1.6.0" + +bytecheck = { version = "0.6.12", default-features = false } +rand = { version = "0.8.5", default-features = false } +rkyv = { version = "0.7.39", default-features = false } +serde = { version = "1", default-features = false, features = [ + "derive", + "alloc", +] } +serde_json = { version = "1", default-features = false } +serde_with = { version = "=3.9.0", default-features = false, features = [ + "hex", +] } +time = { version = "=0.3.36", default-features = false } + +[profile.dev.build-override] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 + +[profile.release.build-override] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 diff --git a/standards/LICENSE b/standards/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/standards/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml new file mode 100644 index 00000000..cf0171d1 --- /dev/null +++ b/standards/dusk-contract-standards/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "dusk-contract-standards" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +dusk-core = "=1.6.0" +dusk-forge = "=0.3.0" +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = [ + "derive", + "alloc", +], optional = true } +serde_with = { version = "=3.9.0", default-features = false, features = [ + "hex", +], optional = true } +time = { version = "=0.3.36", default-features = false, optional = true } + +[features] +contract = [] +serde = ["dep:serde", "dep:serde_with", "dep:time", "dusk-core/serde"] + +[dev-dependencies] +dusk-data-driver = { version = "=0.3.2-alpha.1", features = ["reader"] } +dusk-vm = "=1.6.0" +proptest = "1.6.0" +rand = { workspace = true, features = ["std_rng"] } +serde_json = { version = "1", default-features = false, features = ["std"] } diff --git a/standards/dusk-contract-standards/Makefile b/standards/dusk-contract-standards/Makefile new file mode 100644 index 00000000..bd829562 --- /dev/null +++ b/standards/dusk-contract-standards/Makefile @@ -0,0 +1,17 @@ +all: check test clippy + +check: + @cargo check -p dusk-contract-standards + +test: + @cargo test -p dusk-contract-standards + +wasm: + +clippy: + @cargo clippy -p dusk-contract-standards -- -D warnings + +doc: + @cargo doc -p dusk-contract-standards --no-deps + +.PHONY: all check test wasm clippy doc diff --git a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs new file mode 100644 index 00000000..05345c8b --- /dev/null +++ b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs @@ -0,0 +1,170 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::vec::Vec; + +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const CHAIN_ID: u8 = 0xFA; +const EXAMPLE_EXPIRES_AT: u64 = 1_000; + +fn main() { + let contract = ContractId::from_bytes([42u8; 32]); + let spender = Principal::Contract(ContractId::from_bytes([7u8; 32])); + + let moonlight_sk = moonlight_secret(11); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight_owner = Principal::moonlight(&moonlight_pk); + let moonlight_action = + drc20_signed_approve_action(contract, moonlight_owner, spender, 100, 0); + let moonlight = SignedAuthorization::Moonlight(MoonlightAuthorization { + action: moonlight_action, + public_key: moonlight_pk, + signature: moonlight_sk.sign(&moonlight_action.message_bytes()), + }); + moonlight.assert_action( + CHAIN_ID, + contract, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + drc20_approve_payload_hash(moonlight_owner, spender, 100), + ); + + let mut rng = StdRng::seed_from_u64(99); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(12u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix_owner = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = + drc20_signed_approve_action(contract, phoenix_owner, spender, 100, 0); + let phoenix = SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + phoenix.assert_action( + CHAIN_ID, + contract, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + drc20_approve_payload_hash(phoenix_owner, spender, 100), + ); + + let nft_approved = Principal::Contract(ContractId::from_bytes([8u8; 32])); + let nft_action = drc721_signed_approve_action( + contract, + phoenix_owner, + nft_approved, + 42, + 1, + ); + let nft_approval = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: nft_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, nft_action.message_hash()), + replay_key: None, + }); + nft_approval.assert_action( + CHAIN_ID, + contract, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + drc721_approve_payload_hash(phoenix_owner, nft_approved, 42), + ); +} + +fn drc20_signed_approve_action( + contract: ContractId, + owner: Principal, + spender: Principal, + amount: u64, + nonce: u64, +) -> AuthorizedAction { + AuthorizedAction { + chain_id: CHAIN_ID, + contract, + domain: SIGNED_APPROVE_DOMAIN, + action_id: SIGNED_APPROVE_ACTION, + nonce, + expires_at: EXAMPLE_EXPIRES_AT, + principal: owner, + payload_hash: drc20_approve_payload_hash(owner, spender, amount), + } +} + +fn drc20_approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn drc721_signed_approve_action( + contract: ContractId, + owner: Principal, + approved: Principal, + token_id: u64, + nonce: u64, +) -> AuthorizedAction { + AuthorizedAction { + chain_id: CHAIN_ID, + contract, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: NFT_SIGNED_APPROVE_ACTION, + nonce, + expires_at: EXAMPLE_EXPIRES_AT, + principal: owner, + payload_hash: drc721_approve_payload_hash(owner, approved, token_id), + } +} + +fn drc721_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs new file mode 100644 index 00000000..fb78583c --- /dev/null +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -0,0 +1,968 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::env; +use std::error::Error; +use std::string::String; +use std::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::Principal; +use dusk_contract_standards::governance::{ + MultisigControllerConfig, MultisigOperationId, MultisigTarget, +}; +use dusk_contract_standards::token::drc20::{ + Init as Drc20TokenInit, InitBalance as Drc20InitBalance, +}; +use dusk_contract_standards::token::drc721::{ + Init as Drc721TokenInit, InitToken as Drc721InitToken, IsApprovedForAll, + SignedApproveCall as Drc721SignedApproveCall, + SignedSetApprovalForAllCall as Drc721SignedSetApprovalForAllCall, +}; +use dusk_core::abi::{ContractId, StandardBufSerializer, ARGBUF_LEN}; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::transfer::data::ContractCall; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rkyv::ser::serializers::{ + BufferScratch, BufferSerializer, CompositeSerializer, +}; +use rkyv::ser::Serializer; +use rkyv::{Archive, Deserialize, Infallible, Serialize}; + +const SCRATCH_BUF_BYTES: usize = 1024; +const SET_VALUE_DOMAIN: [u8; 32] = [3u8; 32]; +const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; +const TOKEN_ADMIN_DOMAIN: [u8; 32] = [11u8; 32]; +const MINT_ACTION: [u8; 32] = [13u8; 32]; +const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +const NFT_ADMIN_DOMAIN: [u8; 32] = [21u8; 32]; +const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; +const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; +const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: [u8; 32] = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; +const PROXY_ADMIN_DOMAIN: [u8; 32] = [31u8; 32]; +const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; +const MULTISIG_CONTROLLER_DOMAIN: [u8; 32] = [41u8; 32]; +const MULTISIG_PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +const MULTISIG_CONFIRM_ACTION: [u8; 32] = [43u8; 32]; + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20ExampleInit { + admin: Principal, + token: Drc20TokenInit, + cap: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721ExampleInit { + owner: Principal, + token: Drc721TokenInit, + default_royalty: + Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct ProxyCounterInit { + admin: Principal, + implementation: ContractId, + upgrade_delay: u64, + rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct MultisigInit { + config: MultisigControllerConfig, +} + +#[derive(Archive, Serialize, Deserialize)] +struct SetValueByMoonlight { + authorization: MoonlightAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct NonceQuery { + principal: Principal, + domain: [u8; 32], +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20MintCall { + to: Principal, + amount: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721MintCall { + to: Principal, + token_id: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +struct ProxySetValue { + value: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct MultisigPropose { + target: MultisigTarget, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct MultisigConfirm { + id: MultisigOperationId, + authorization: Option, +} + +struct AdminPhoenixAction { + contract: ContractId, + admin: Principal, + domain: [u8; 32], + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +} + +fn main() -> Result<(), Box> { + let args = env::args().collect::>(); + let Some(command) = args.get(1).map(String::as_str) else { + eprintln!( + "usage: encode_local_smoke_args " + ); + std::process::exit(2); + }; + + let admin = phoenix_principal(1); + let bytes = match command { + "drc20-init" => encode(&Drc20ExampleInit { + admin, + token: Drc20TokenInit { + name: String::from("Local Dusk Token"), + symbol: String::from("LDUSK"), + decimals: 9, + initial_balances: vec![Drc20InitBalance { + account: admin, + amount: 1_000, + }], + }, + cap: 10_000_000, + })?, + "drc721-init" => encode(&Drc721ExampleInit { + owner: admin, + token: Drc721TokenInit { + name: String::from("Local Dusk Collection"), + symbol: String::from("LDNFT"), + base_uri: String::from("ipfs://local/"), + initial_tokens: vec![Drc721InitToken { + account: admin, + token_id: 1, + }], + }, + default_royalty: None, + })?, + "proxy-init" => encode(&ProxyCounterInit { + admin, + implementation: ContractId::from_bytes([9u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + })?, + "multisig-init" => encode(&MultisigInit { + config: MultisigControllerConfig { + owners: vec![admin, phoenix_principal(2), phoenix_principal(3)], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 6, + }, + })?, + "proxy-init-contract" => encode(&ProxyCounterInit { + admin: Principal::contract(contract_arg(&args, 2)?), + implementation: ContractId::from_bytes([9u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + })?, + "unit" => encode(&())?, + "u64" => encode(&parse_u64_arg(&args, 2, "value")?)?, + "nonce" => encode(&NonceQuery { + principal: principal_arg(&args, 3)?, + domain: domain_arg(&args, 2)?, + })?, + "auth-counter-phoenix" => encode(&counter_phoenix_call(&args)?)?, + "auth-counter-moonlight" => encode(&counter_moonlight_call(&args)?)?, + "drc20-mint" => encode(&drc20_mint_call(&args, admin)?)?, + "drc20-admin" => encode(&drc20_admin_call(&args, admin)?)?, + "drc721-mint" => encode(&drc721_mint_call(&args, admin)?)?, + "drc721-admin" => encode(&drc721_admin_call(&args, admin)?)?, + "drc721-approve" => encode(&drc721_approve_call(&args, admin)?)?, + "drc721-operator-approval" => { + encode(&drc721_operator_approval_call(&args, admin)?)? + } + "drc721-operator-query" => encode(&IsApprovedForAll { + owner: admin, + operator: drc721_operator_principal(), + })?, + "proxy-set" => encode(&proxy_set_call(&args, admin)?)?, + "proxy-set-no-auth" => encode(&ProxySetValue { + value: parse_u64_arg(&args, 2, "value")?, + authorization: None, + })?, + "multisig-target" => encode(&multisig_target_arg(&args, 2, 3, 4)?)?, + "multisig-propose" => encode(&multisig_propose_call(&args)?)?, + "multisig-confirm" => encode(&multisig_confirm_call(&args)?)?, + _ => { + eprintln!("unknown command: {command}"); + std::process::exit(2); + } + }; + + print_hex(&bytes); + Ok(()) +} + +fn counter_phoenix_call( + args: &[String], +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let public = SchnorrPublicKey::from(&secret); + let principal = Principal::phoenix_public_key(&public); + let action = counter_action( + contract_for_variant(contract, variant), + principal, + nonce, + expires_at, + action_for_variant(SET_VALUE_ACTION, variant), + payload_amount_for_variant(amount, variant), + ); + let mut rng = StdRng::seed_from_u64(12_345 + nonce); + Ok(SetValueByPhoenix { + authorization: phoenix_auth(&mut rng, &secret, public, action), + amount, + }) +} + +fn counter_moonlight_call( + args: &[String], +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = moonlight_secret(7); + let public = BlsPublicKey::from(&secret); + let principal = Principal::moonlight(&public); + let action = counter_action( + contract_for_variant(contract, variant), + principal, + nonce, + expires_at, + action_for_variant(SET_VALUE_ACTION, variant), + payload_amount_for_variant(amount, variant), + ); + Ok(SetValueByMoonlight { + authorization: moonlight_auth(&secret, public, action), + amount, + }) +} + +fn drc20_mint_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = SchnorrSecretKey::from(JubJubScalar::from(1u64)); + let public = SchnorrPublicKey::from(&secret); + let payload_amount = payload_amount_for_variant(amount, variant); + let action = authorized_action( + contract_for_variant(contract, variant), + admin, + TOKEN_ADMIN_DOMAIN, + action_for_variant(MINT_ACTION, variant), + nonce, + expires_at, + mint_payload_hash(admin, payload_amount), + ); + let mut rng = StdRng::seed_from_u64(22_222 + nonce); + Ok(Drc20MintCall { + to: admin, + amount, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn drc20_admin_call( + args: &[String], + admin: Principal, +) -> Result> { + let action_name = arg(args, 2, "action")?; + let contract = contract_arg(args, 3)?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let variant = variant_arg(args); + let (action_id, tag) = match action_name { + "pause" => (PAUSE_ACTION, b"drc20.pause".as_slice()), + "unpause" => (UNPAUSE_ACTION, b"drc20.unpause".as_slice()), + _ => { + return Err( + format!("unknown drc20 admin action: {action_name}").into() + ) + } + }; + Ok(Drc20AdminCall { + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: TOKEN_ADMIN_DOMAIN, + action_id: action_for_variant(action_id, variant), + nonce, + expires_at, + payload_hash: empty_payload_hash(tag), + }, + variant, + )), + }) +} + +fn drc721_mint_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let token_id = parse_u64_arg(args, 5, "token_id")?; + let variant = variant_arg(args); + let payload_token_id = payload_amount_for_variant(token_id, variant); + Ok(Drc721MintCall { + to: admin, + token_id, + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_ADMIN_DOMAIN, + action_id: action_for_variant(NFT_MINT_ACTION, variant), + nonce, + expires_at, + payload_hash: nft_mint_payload_hash(admin, payload_token_id), + }, + variant, + )), + }) +} + +fn drc721_admin_call( + args: &[String], + admin: Principal, +) -> Result> { + let action_name = arg(args, 2, "action")?; + let contract = contract_arg(args, 3)?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let variant = variant_arg(args); + let (action_id, tag) = match action_name { + "pause" => (NFT_PAUSE_ACTION, b"drc721.pause".as_slice()), + "unpause" => (NFT_UNPAUSE_ACTION, b"drc721.unpause".as_slice()), + _ => { + return Err( + format!("unknown drc721 admin action: {action_name}").into() + ) + } + }; + Ok(Drc721AdminCall { + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_ADMIN_DOMAIN, + action_id: action_for_variant(action_id, variant), + nonce, + expires_at, + payload_hash: empty_payload_hash(tag), + }, + variant, + )), + }) +} + +fn drc721_approve_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let token_id = parse_u64_arg(args, 5, "token_id")?; + let variant = variant_arg(args); + let approved = drc721_approved_principal(); + let payload_token_id = payload_amount_for_variant(token_id, variant); + Ok(Drc721SignedApproveCall { + owner: admin, + approved, + token_id, + authorization: admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: action_for_variant( + NFT_SIGNED_APPROVE_ACTION, + variant, + ), + nonce, + expires_at, + payload_hash: nft_approve_payload_hash( + admin, + approved, + payload_token_id, + ), + }, + variant, + ), + }) +} + +fn drc721_operator_approval_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let approved = parse_bool_arg(args, 5, "approved")?; + let variant = variant_arg(args); + let operator = drc721_operator_principal(); + let payload_approved = payload_bool_for_variant(approved, variant); + Ok(Drc721SignedSetApprovalForAllCall { + owner: admin, + operator, + approved, + authorization: admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: action_for_variant( + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + variant, + ), + nonce, + expires_at, + payload_hash: nft_approval_for_all_payload_hash( + admin, + operator, + payload_approved, + ), + }, + variant, + ), + }) +} + +fn proxy_set_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let value = parse_u64_arg(args, 5, "value")?; + let variant = variant_arg(args); + let payload_value = payload_amount_for_variant(value, variant); + Ok(ProxySetValue { + value, + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: PROXY_ADMIN_DOMAIN, + action_id: action_for_variant(SET_PROXY_VALUE_ACTION, variant), + nonce, + expires_at, + payload_hash: proxy_value_payload_hash(payload_value), + }, + variant, + )), + }) +} + +fn multisig_propose_call( + args: &[String], +) -> Result> { + let multisig = contract_arg(args, 2)?; + let owner_seed = parse_u64_arg(args, 4, "owner_seed")?; + let nonce = parse_u64_arg(args, 5, "nonce")?; + let expires_at = parse_u64_arg(args, 6, "expires_at")?; + let id = parse_hex_32(arg(args, 9, "operation_id")?)?; + let variant = args.get(10).map(String::as_str).unwrap_or("valid"); + let owner = phoenix_principal(owner_seed); + let secret = SchnorrSecretKey::from(JubJubScalar::from(owner_seed)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + multisig, + owner, + MULTISIG_CONTROLLER_DOMAIN, + action_for_variant(MULTISIG_PROPOSE_ACTION, variant), + nonce, + expires_at, + operation_id_for_variant(id, variant), + ); + let mut rng = StdRng::seed_from_u64(44_444 + owner_seed + nonce); + Ok(MultisigPropose { + target: multisig_target_arg(args, 3, 7, 8)?, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn multisig_confirm_call( + args: &[String], +) -> Result> { + let multisig = contract_arg(args, 2)?; + let owner_seed = parse_u64_arg(args, 3, "owner_seed")?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let id = parse_hex_32(arg(args, 6, "operation_id")?)?; + let variant = args.get(7).map(String::as_str).unwrap_or("valid"); + let owner = phoenix_principal(owner_seed); + let secret = SchnorrSecretKey::from(JubJubScalar::from(owner_seed)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + multisig, + owner, + MULTISIG_CONTROLLER_DOMAIN, + action_for_variant(MULTISIG_CONFIRM_ACTION, variant), + nonce, + expires_at, + operation_id_for_variant(id, variant), + ); + let mut rng = StdRng::seed_from_u64(55_555 + owner_seed + nonce); + Ok(MultisigConfirm { + id, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn multisig_target_arg( + args: &[String], + proxy_index: usize, + value_index: usize, + salt_index: usize, +) -> Result> { + let proxy = contract_arg(args, proxy_index)?; + let value = parse_u64_arg(args, value_index, "value")?; + let salt_seed = parse_u8_arg(args, salt_index, "salt_seed")?; + proxy_set_value_target(proxy, value, [salt_seed; 32]) +} + +fn proxy_set_value_target( + proxy: ContractId, + value: u64, + salt: [u8; 32], +) -> Result> { + Ok(MultisigTarget { + call: ContractCall::new(proxy, "set_value") + .with_args(&ProxySetValue { + value, + authorization: None, + }) + .map_err(|e| format!("serialize proxy set_value target: {e:?}"))?, + salt, + }) +} + +fn admin_phoenix_authorization( + args: AdminPhoenixAction, + variant: &str, +) -> SignedAuthorization { + let secret = SchnorrSecretKey::from(JubJubScalar::from(1u64)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + contract_for_variant(args.contract, variant), + args.admin, + args.domain, + args.action_id, + args.nonce, + args.expires_at, + args.payload_hash, + ); + let mut rng = StdRng::seed_from_u64(33_333 + args.nonce); + SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + )) +} + +fn counter_action( + contract: ContractId, + principal: Principal, + nonce: u64, + expires_at: u64, + action_id: [u8; 32], + amount: u64, +) -> AuthorizedAction { + authorized_action( + contract, + principal, + SET_VALUE_DOMAIN, + action_id, + nonce, + expires_at, + amount_hash(amount), + ) +} + +fn authorized_action( + contract: ContractId, + principal: Principal, + domain: [u8; 32], + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +) -> AuthorizedAction { + AuthorizedAction { + chain_id: chain_id(), + contract, + domain, + action_id, + nonce, + expires_at, + principal, + payload_hash, + } +} + +fn chain_id() -> u8 { + env::var("DUSK_CHAIN_ID") + .unwrap_or_else(|_| "250".to_string()) + .parse() + .expect("DUSK_CHAIN_ID must be a u8") +} + +fn phoenix_auth( + rng: &mut StdRng, + secret_key: &SchnorrSecretKey, + public_key: SchnorrPublicKey, + action: AuthorizedAction, +) -> PhoenixSignatureAuthorization { + PhoenixSignatureAuthorization { + action, + public_key, + signature: secret_key.sign(rng, action.message_hash()), + replay_key: None, + } +} + +fn moonlight_auth( + secret_key: &BlsSecretKey, + public_key: BlsPublicKey, + action: AuthorizedAction, +) -> MoonlightAuthorization { + MoonlightAuthorization { + action, + public_key, + signature: secret_key.sign(&action.message_bytes()), + } +} + +fn amount_hash(amount: u64) -> [u8; 32] { + host_queries::keccak256(Vec::from(amount.to_be_bytes())) +} + +fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + host_queries::keccak256(bytes) +} + +fn proxy_value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + host_queries::keccak256(Vec::from(tag)) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} + +fn phoenix_principal(seed: u64) -> Principal { + let secret = SchnorrSecretKey::from(JubJubScalar::from(seed)); + let public = SchnorrPublicKey::from(&secret); + Principal::phoenix_public_key(&public) +} + +fn moonlight_principal(seed: u64) -> Principal { + let secret = moonlight_secret(seed); + let public = BlsPublicKey::from(&secret); + Principal::moonlight(&public) +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn contract_for_variant(contract: ContractId, variant: &str) -> ContractId { + if variant == "wrong-contract" { + ContractId::from_bytes([0xee; 32]) + } else { + contract + } +} + +fn action_for_variant(action_id: [u8; 32], variant: &str) -> [u8; 32] { + if variant == "wrong-action" { + [0xaa; 32] + } else { + action_id + } +} + +fn payload_amount_for_variant(amount: u64, variant: &str) -> u64 { + if variant == "bad-payload" { + amount.saturating_add(1) + } else { + amount + } +} + +fn payload_bool_for_variant(value: bool, variant: &str) -> bool { + if variant == "bad-payload" { + !value + } else { + value + } +} + +fn operation_id_for_variant( + id: MultisigOperationId, + variant: &str, +) -> MultisigOperationId { + if variant == "bad-payload" { + [0xee; 32] + } else { + id + } +} + +fn variant_arg(args: &[String]) -> &str { + args.get(6).map(String::as_str).unwrap_or("valid") +} + +fn arg<'a>( + args: &'a [String], + index: usize, + name: &str, +) -> Result<&'a str, Box> { + args.get(index) + .map(String::as_str) + .ok_or_else(|| format!("missing {name}").into()) +} + +fn parse_u64_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + arg(args, index, name)? + .parse::() + .map_err(|e| format!("invalid {name}: {e}").into()) +} + +fn parse_u8_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + arg(args, index, name)? + .parse::() + .map_err(|e| format!("invalid {name}: {e}").into()) +} + +fn contract_arg( + args: &[String], + index: usize, +) -> Result> { + Ok(ContractId::from_bytes(parse_hex_32(arg( + args, index, "contract", + )?)?)) +} + +fn principal_arg( + args: &[String], + index: usize, +) -> Result> { + let kind = arg(args, index, "principal kind")?; + let seed = parse_u64_arg(args, index + 1, "principal seed")?; + match kind { + "phoenix" => Ok(phoenix_principal(seed)), + "moonlight" => Ok(moonlight_principal(seed)), + _ => Err(format!("unknown principal kind: {kind}").into()), + } +} + +fn parse_bool_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + match arg(args, index, name)? { + "true" => Ok(true), + "false" => Ok(false), + value => Err(format!("invalid {name}: {value}").into()), + } +} + +fn domain_arg( + args: &[String], + index: usize, +) -> Result<[u8; 32], Box> { + match arg(args, index, "domain")? { + "counter" => Ok(SET_VALUE_DOMAIN), + "drc20-admin" => Ok(TOKEN_ADMIN_DOMAIN), + "drc721-admin" => Ok(NFT_ADMIN_DOMAIN), + "drc721-approval" => Ok(NFT_SIGNED_APPROVE_DOMAIN), + "proxy-admin" => Ok(PROXY_ADMIN_DOMAIN), + "multisig" => Ok(MULTISIG_CONTROLLER_DOMAIN), + other => Err(format!("unknown domain: {other}").into()), + } +} + +fn drc721_approved_principal() -> Principal { + Principal::Contract(ContractId::from_bytes([55u8; 32])) +} + +fn drc721_operator_principal() -> Principal { + Principal::Contract(ContractId::from_bytes([56u8; 32])) +} + +fn parse_hex_32(hex: &str) -> Result<[u8; 32], Box> { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + if hex.len() != 64 { + return Err(format!("expected 64 hex chars, got {}", hex.len()).into()); + } + let mut bytes = [0u8; 32]; + for i in 0..32 { + bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) + .map_err(|e| format!("invalid contract hex: {e}"))?; + } + Ok(bytes) +} + +fn encode(args: &A) -> Result, Box> +where + A: for<'a> Serialize>, +{ + let mut buf = vec![0u8; ARGBUF_LEN]; + let mut scratch = [0u8; SCRATCH_BUF_BYTES]; + let pos = { + let ser = BufferSerializer::new(buf.as_mut_slice()); + let scratch = BufferScratch::new(&mut scratch); + let mut serializer = CompositeSerializer::new(ser, scratch, Infallible); + serializer + .serialize_value(args) + .map_err(|e| format!("serialize args failed: {e:?}"))?; + serializer.pos() + }; + buf.truncate(pos); + Ok(buf) +} + +fn print_hex(bytes: &[u8]) { + for byte in bytes { + print!("{byte:02x}"); + } + println!(); +} diff --git a/standards/dusk-contract-standards/src/access/access_control.rs b/standards/dusk-contract-standards/src/access/access_control.rs new file mode 100644 index 00000000..0e37a4ce --- /dev/null +++ b/standards/dusk-contract-standards/src/access/access_control.rs @@ -0,0 +1,221 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Role-based access control. + +use alloc::collections::{BTreeMap, BTreeSet}; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// 32-byte role id. +pub type Role = [u8; 32]; + +/// Default admin role. +pub const DEFAULT_ADMIN_ROLE: Role = [0u8; 32]; + +#[derive(Clone, Debug)] +struct RoleData { + members: BTreeSet, + admin_role: Role, +} + +impl RoleData { + fn new(admin_role: Role) -> Self { + Self { + members: BTreeSet::new(), + admin_role, + } + } +} + +/// Role-based access-control module. +#[derive(Clone, Debug, Default)] +pub struct AccessControl { + roles: BTreeMap, +} + +impl AccessControl { + /// Creates an empty access-control module. + pub const fn new() -> Self { + Self { + roles: BTreeMap::new(), + } + } + + /// Bootstraps the default admin role. + pub fn init_admin(&mut self, admin: Principal) { + if admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if self.roles.contains_key(&DEFAULT_ADMIN_ROLE) { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.roles + .entry(DEFAULT_ADMIN_ROLE) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .members + .insert(admin); + } + + /// Returns true when `account` has `role`. + pub fn has_role(&self, role: Role, account: Principal) -> bool { + self.roles + .get(&role) + .map(|data| data.members.contains(&account)) + .unwrap_or(false) + } + + /// Panics unless `account` has `role`. + pub fn assert_role(&self, role: Role, account: Principal) { + if !self.has_role(role, account) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes any principal with `role` through runtime context or signed + /// authorization. + pub fn authorize_role( + &self, + role: Role, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_role_with(role, &mut authorizer, authorization) + } + + /// Authorizes any principal with `role` using a reusable call authorizer. + pub fn authorize_role_with( + &self, + role: Role, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.has_role(role, principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + authorizer.require_unbound_signed_if(authorization, |principal| { + self.has_role(role, principal) + }) + } + + /// Authorizes any principal with `role` through runtime context or an + /// action-bound signed authorization. + pub fn authorize_role_action( + &self, + role: Role, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_role_action_with( + role, + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes any principal with `role` using a reusable call authorizer + /// and exact call envelope for signed fallbacks. + pub fn authorize_role_action_with( + &self, + role: Role, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.has_role(role, principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + authorizer.require_signed_action_if( + authorization, + envelope, + |principal| self.has_role(role, principal), + ) + } + + /// Returns a role's admin role. + pub fn get_role_admin(&self, role: Role) -> Role { + self.roles + .get(&role) + .map(|data| data.admin_role) + .unwrap_or(DEFAULT_ADMIN_ROLE) + } + + /// Sets a role's admin role. Caller must have the current admin role. + pub fn set_role_admin( + &mut self, + caller: Principal, + role: Role, + admin_role: Role, + ) { + let current_admin = self.get_role_admin(role); + self.assert_role(current_admin, caller); + self.roles + .entry(role) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .admin_role = admin_role; + } + + /// Grants `role` to `account`. Caller must have the role admin. + pub fn grant_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + if account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let admin = self.get_role_admin(role); + self.assert_role(admin, caller); + self.roles + .entry(role) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .members + .insert(account); + } + + /// Revokes `role` from `account`. Caller must have the role admin. + pub fn revoke_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + let admin = self.get_role_admin(role); + self.assert_role(admin, caller); + if let Some(data) = self.roles.get_mut(&role) { + data.members.remove(&account); + } + } + + /// Renounces `role` from `caller`. + pub fn renounce_role(&mut self, role: Role, caller: Principal) { + if let Some(data) = self.roles.get_mut(&role) { + data.members.remove(&caller); + } + } +} diff --git a/standards/dusk-contract-standards/src/access/events.rs b/standards/dusk-contract-standards/src/access/events.rs new file mode 100644 index 00000000..670b1c13 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/events.rs @@ -0,0 +1,91 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Access-control and emergency-stop event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +use super::Role; + +/// Pause event topic. +pub const PAUSED_TOPIC: &str = "access/paused"; +/// Unpause event topic. +pub const UNPAUSED_TOPIC: &str = "access/unpaused"; +/// Role-grant event topic. +pub const ROLE_GRANTED_TOPIC: &str = "access/role_granted"; +/// Role-revoke event topic. +pub const ROLE_REVOKED_TOPIC: &str = "access/role_revoked"; + +/// Pause event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Paused { + /// Principal that authorized the pause. + pub account: Principal, +} + +/// Unpause event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Unpaused { + /// Principal that authorized the unpause. + pub account: Principal, +} + +/// Role-grant event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleGranted { + /// Role id. + pub role: Role, + /// Principal receiving the role. + pub account: Principal, + /// Principal that authorized the grant. + pub sender: Principal, +} + +/// Role-revoke event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleRevoked { + /// Role id. + pub role: Role, + /// Principal losing the role. + pub account: Principal, + /// Principal that authorized the revoke. + pub sender: Principal, +} + +impl dusk_forge::ContractEvent for Paused { + const TOPICS: &'static [&'static str] = &[PAUSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for Unpaused { + const TOPICS: &'static [&'static str] = &[UNPAUSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for RoleGranted { + const TOPICS: &'static [&'static str] = &[ROLE_GRANTED_TOPIC]; +} + +impl dusk_forge::ContractEvent for RoleRevoked { + const TOPICS: &'static [&'static str] = &[ROLE_REVOKED_TOPIC]; +} diff --git a/standards/dusk-contract-standards/src/access/mod.rs b/standards/dusk-contract-standards/src/access/mod.rs new file mode 100644 index 00000000..7d11135c --- /dev/null +++ b/standards/dusk-contract-standards/src/access/mod.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Access-control modules. + +pub mod access_control; +pub mod events; +pub mod ownable; +pub mod owner_set; +pub mod pausable; + +pub use access_control::{AccessControl, Role, DEFAULT_ADMIN_ROLE}; +pub use ownable::{Ownable, Ownable2Step}; +pub use owner_set::OwnerSet; +pub use pausable::Pausable; diff --git a/standards/dusk-contract-standards/src/access/ownable.rs b/standards/dusk-contract-standards/src/access/ownable.rs new file mode 100644 index 00000000..0c67de6d --- /dev/null +++ b/standards/dusk-contract-standards/src/access/ownable.rs @@ -0,0 +1,252 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Ownership primitives. + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// Single-owner access control. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ownable { + owner: Option, +} + +impl Ownable { + /// Creates an uninitialized owner module. + pub const fn new() -> Self { + Self { owner: None } + } + + /// Initializes ownership. + pub fn init(&mut self, owner: Principal) { + if self.owner.is_some() { + panic!("{}", error::ALREADY_INITIALIZED); + } + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owner = Some(owner); + } + + /// Returns the current owner. + pub const fn owner(&self) -> Option { + self.owner + } + + /// Returns true when `caller` is the owner. + pub fn is_owner(&self, caller: Principal) -> bool { + self.owner == Some(caller) + } + + /// Panics unless `caller` is the owner. + pub fn assert_owner(&self, caller: Principal) { + if !self.is_owner(caller) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes the owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_with(&mut authorizer, authorization) + } + + /// Authorizes the owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + let owner = self + .owner + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)); + authorizer.require_principal_unbound(owner, authorization) + } + + /// Authorizes the owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes the owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + let owner = self + .owner + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)); + authorizer.require_principal_action(owner, authorization, envelope) + } + + /// Transfers ownership. + pub fn transfer_ownership( + &mut self, + caller: Principal, + new_owner: Principal, + ) { + self.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owner = Some(new_owner); + } + + /// Renounces ownership. + pub fn renounce_ownership(&mut self, caller: Principal) { + self.assert_owner(caller); + self.owner = None; + } +} + +/// Two-step ownership transfer. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ownable2Step { + ownable: Ownable, + pending_owner: Option, +} + +impl Ownable2Step { + /// Creates an uninitialized module. + pub const fn new() -> Self { + Self { + ownable: Ownable::new(), + pending_owner: None, + } + } + + /// Initializes ownership. + pub fn init(&mut self, owner: Principal) { + self.ownable.init(owner); + } + + /// Returns the current owner. + pub const fn owner(&self) -> Option { + self.ownable.owner() + } + + /// Returns the pending owner. + pub const fn pending_owner(&self) -> Option { + self.pending_owner + } + + /// Panics unless `caller` is the owner. + pub fn assert_owner(&self, caller: Principal) { + self.ownable.assert_owner(caller); + } + + /// Authorizes the owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + self.ownable.authorize_owner( + authorizations, + context, + authorization, + now, + ) + } + + /// Authorizes the owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + self.ownable.authorize_owner_with(authorizer, authorization) + } + + /// Authorizes the owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + self.ownable.authorize_owner_action( + authorizations, + context, + authorization, + envelope, + now, + ) + } + + /// Authorizes the owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.ownable.authorize_owner_action_with( + authorizer, + authorization, + envelope, + ) + } + + /// Starts ownership transfer. + pub fn transfer_ownership( + &mut self, + caller: Principal, + new_owner: Principal, + ) { + self.ownable.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.pending_owner = Some(new_owner); + } + + /// Accepts ownership transfer. + pub fn accept_ownership(&mut self, caller: Principal) { + if self.pending_owner != Some(caller) { + panic!("{}", error::UNAUTHORIZED); + } + self.ownable.owner = Some(caller); + self.pending_owner = None; + } + + /// Renounces ownership. + pub fn renounce_ownership(&mut self, caller: Principal) { + self.ownable.renounce_ownership(caller); + self.pending_owner = None; + } +} diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs new file mode 100644 index 00000000..f93e5f13 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -0,0 +1,202 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Multi-principal ownership. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal, PrincipalKind}; + +/// Owner registry for contracts that need Moonlight, Phoenix, and contract +/// identities to coexist in one authorization policy. +#[derive(Clone, Debug, Default)] +pub struct OwnerSet { + owners: BTreeSet, +} + +impl OwnerSet { + /// Creates an empty owner set. + pub const fn new() -> Self { + Self { + owners: BTreeSet::new(), + } + } + + /// Initializes with at least one owner. + pub fn init(&mut self, owners: impl IntoIterator) { + if !self.owners.is_empty() { + panic!("{}", error::ALREADY_INITIALIZED); + } + let mut next = BTreeSet::new(); + for owner in owners { + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + next.insert(owner); + } + if next.is_empty() { + panic!("{}", error::INVALID_OWNER); + } + self.owners = next; + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.owners.contains(&principal) + } + + /// Panics unless `principal` is an owner. + pub fn assert_owner(&self, principal: Principal) { + if !self.is_owner(principal) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes any owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_with(&mut authorizer, authorization) + } + + /// Authorizes any owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.is_owner(principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + authorizer.require_unbound_signed_if(authorization, |principal| { + self.is_owner(principal) + }) + } + + /// Authorizes any owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes any owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.is_owner(principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + authorizer.require_signed_action_if( + authorization, + envelope, + |principal| self.is_owner(principal), + ) + } + + /// Returns all owners in stable order. + pub fn owners(&self) -> Vec { + self.owners.iter().copied().collect() + } + + /// Returns the number of owners. + pub fn len(&self) -> usize { + self.owners.len() + } + + /// Returns true when no owners are configured. + pub fn is_empty(&self) -> bool { + self.owners.is_empty() + } + + /// Counts owners of a given principal kind. + pub fn count_kind(&self, kind: PrincipalKind) -> usize { + self.owners + .iter() + .filter(|principal| principal.kind() == kind) + .count() + } + + /// Adds an owner. Caller must already be an owner. + pub fn add_owner(&mut self, caller: Principal, new_owner: Principal) { + self.assert_owner(caller); + self.add_initial_owner(new_owner); + } + + /// Removes an owner. The last owner cannot be removed. + pub fn remove_owner(&mut self, caller: Principal, owner: Principal) { + self.assert_owner(caller); + if !self.owners.contains(&owner) { + panic!("{}", error::INVALID_OWNER); + } + if self.owners.len() == 1 { + panic!("{}", error::INVALID_OWNER); + } + self.owners.remove(&owner); + } + + /// Replaces one owner with another. + pub fn replace_owner( + &mut self, + caller: Principal, + old_owner: Principal, + new_owner: Principal, + ) { + self.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !self.owners.contains(&old_owner) { + panic!("{}", error::INVALID_OWNER); + } + if old_owner != new_owner && self.owners.contains(&new_owner) { + panic!("{}", error::INVALID_OWNER); + } + self.owners.remove(&old_owner); + self.owners.insert(new_owner); + } + + fn add_initial_owner(&mut self, owner: Principal) { + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owners.insert(owner); + } +} diff --git a/standards/dusk-contract-standards/src/access/pausable.rs b/standards/dusk-contract-standards/src/access/pausable.rs new file mode 100644 index 00000000..5aa2412b --- /dev/null +++ b/standards/dusk-contract-standards/src/access/pausable.rs @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Emergency stop primitive. + +use crate::core::error; + +/// Pausable module. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Pausable { + paused: bool, +} + +impl Pausable { + /// Creates an unpaused module. + pub const fn new() -> Self { + Self { paused: false } + } + + /// Returns whether the module is paused. + pub const fn paused(&self) -> bool { + self.paused + } + + /// Panics when paused. + pub fn assert_not_paused(&self) { + if self.paused { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Panics when not paused. + pub fn assert_paused(&self) { + if !self.paused { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Pauses. + pub fn pause(&mut self) { + self.paused = true; + } + + /// Unpauses. + pub fn unpause(&mut self) { + self.paused = false; + } +} diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs new file mode 100644 index 00000000..17393e29 --- /dev/null +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -0,0 +1,763 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Replay-protected signed authorization helpers. + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, Signature as BlsSignature, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, Signature as SchnorrSignature, +}; +use dusk_core::BlsScalar; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{ + error, CallContext, NonceDomain, NonceEntry, NonceManager, Principal, + PrincipalKind, ReplayEntry, ReplayGuard, ReplayKey, +}; + +/// Stable prefix for authorization messages. +pub const AUTH_MESSAGE_PREFIX: &[u8] = b"dusk-contract-standards/auth/v1"; + +/// Expected call envelope for an action-bound authorization. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ActionEnvelope { + /// Dusk chain id this authorization is intended for. + pub chain_id: u8, + /// Contract this authorization is intended for. + pub contract: ContractId, + /// Domain-separated nonce stream. + pub domain: NonceDomain, + /// Application-level action id. + pub action_id: [u8; 32], + /// Hash or commitment of the call payload. + pub payload_hash: [u8; 32], +} + +impl ActionEnvelope { + /// Creates a new expected action envelope. + pub const fn new( + chain_id: u8, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) -> Self { + Self { + chain_id, + contract, + domain, + action_id, + payload_hash, + } + } + + /// Creates an expected action envelope for the current runtime chain. + #[cfg(all(target_family = "wasm", feature = "contract"))] + pub fn for_current_chain( + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) -> Self { + Self::new( + dusk_core::abi::chain_id(), + contract, + domain, + action_id, + payload_hash, + ) + } +} + +/// Contract action authorized by a signature. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AuthorizedAction { + /// Dusk chain id this authorization is intended for. + pub chain_id: u8, + /// Contract this authorization is intended for. + pub contract: ContractId, + /// Domain-separated nonce stream. + pub domain: NonceDomain, + /// Application-level action id. + pub action_id: [u8; 32], + /// Expected nonce. + pub nonce: u64, + /// Expiration timestamp/height unit selected by the contract. + /// + /// A value of zero means no expiry. + pub expires_at: u64, + /// Principal being authorized. + pub principal: Principal, + /// Hash or commitment of the call payload. + pub payload_hash: [u8; 32], +} + +impl AuthorizedAction { + /// Returns true when this action has expired at `now`. + pub const fn is_expired(&self, now: u64) -> bool { + self.expires_at != 0 && now > self.expires_at + } + + /// Builds the stable message bytes signed for this action. + pub fn message_bytes(&self) -> Vec { + let principal = self.principal.to_bytes(); + let mut out = Vec::with_capacity( + AUTH_MESSAGE_PREFIX.len() + + 1 + + 32 + + 32 + + 32 + + 8 + + 8 + + 2 + + principal.len() + + 32, + ); + out.extend_from_slice(AUTH_MESSAGE_PREFIX); + out.push(self.chain_id); + out.extend_from_slice(&self.contract.to_bytes()); + out.extend_from_slice(&self.domain); + out.extend_from_slice(&self.action_id); + out.extend_from_slice(&self.nonce.to_be_bytes()); + out.extend_from_slice(&self.expires_at.to_be_bytes()); + out.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + out.extend_from_slice(&principal); + out.extend_from_slice(&self.payload_hash); + out + } + + /// Hashes the stable message bytes into the scalar signed by Phoenix keys. + pub fn message_hash(&self) -> BlsScalar { + hash_message(self.message_bytes()) + } + + /// Panics unless this action matches the expected contract, nonce domain, + /// action id, and payload hash. + pub fn assert_matches( + &self, + chain_id: u8, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) { + self.assert_envelope(ActionEnvelope::new( + chain_id, + contract, + domain, + action_id, + payload_hash, + )); + } + + /// Panics unless this action matches the expected call envelope. + pub fn assert_envelope(&self, envelope: ActionEnvelope) { + if self.chain_id != envelope.chain_id + || self.contract != envelope.contract + || self.domain != envelope.domain + || self.action_id != envelope.action_id + || self.payload_hash != envelope.payload_hash + { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +/// Moonlight BLS authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MoonlightAuthorization { + /// Authorized action. + pub action: AuthorizedAction, + /// Moonlight public key. + pub public_key: BlsPublicKey, + /// Signature over `action.message_bytes()`. + pub signature: BlsSignature, +} + +/// Phoenix Schnorr authorization. +/// +/// This proves control of the Phoenix Schnorr public key represented by +/// `action.principal`. It does not prove ownership or spending of a specific +/// Phoenix note. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PhoenixSignatureAuthorization { + /// Authorized action. + pub action: AuthorizedAction, + /// Phoenix Schnorr public key. + pub public_key: SchnorrPublicKey, + /// Signature over `action.message_hash()`. + pub signature: SchnorrSignature, + /// Optional extra replay key consumed with the signature. + pub replay_key: Option, +} + +/// Explicit signed authorization supplied with a call. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum SignedAuthorization { + /// Moonlight BLS signed action. + Moonlight(MoonlightAuthorization), + /// Phoenix Schnorr signed action. + Phoenix(PhoenixSignatureAuthorization), +} + +impl SignedAuthorization { + /// Returns the action covered by the signature. + pub const fn action(&self) -> &AuthorizedAction { + match self { + Self::Moonlight(auth) => &auth.action, + Self::Phoenix(auth) => &auth.action, + } + } + + /// Panics unless the signed action matches the exact call envelope. + pub fn assert_action( + &self, + chain_id: u8, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) { + self.assert_envelope(ActionEnvelope::new( + chain_id, + contract, + domain, + action_id, + payload_hash, + )); + } + + /// Panics unless the signed action matches the exact call envelope. + pub fn assert_envelope(&self, envelope: ActionEnvelope) { + self.action().assert_envelope(envelope); + } +} + +/// Ergonomic runtime-or-signature authorizer for composing contracts. +/// +/// Moonlight and contract principals can pass through the observed call +/// context. Phoenix principals must pass through explicit signed +/// authorization because Phoenix calls do not expose a stable caller identity. +pub struct Authorizer<'a> { + authorizations: &'a mut AuthorizationManager, + context: CallContext, + now: u64, +} + +impl<'a> Authorizer<'a> { + /// Creates a reusable authorizer for one contract call. + pub fn new( + authorizations: &'a mut AuthorizationManager, + context: CallContext, + now: u64, + ) -> Self { + Self { + authorizations, + context, + now, + } + } + + /// Returns the observed runtime principal, if any. + pub const fn observed_principal(&self) -> Option { + self.context.principal + } + + /// Verifies an exact expected principal. + /// + /// Signed fallbacks are not checked against a call envelope. Prefer + /// [`Self::require_principal_action`] in public contract methods unless + /// the caller has already bound the action elsewhere. + pub fn require_principal_unbound( + &mut self, + expected: Principal, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + self.authorizations.authorize_principal( + expected, + self.context, + authorization, + self.now, + ) + } + + /// Verifies an exact expected principal and binds signed fallbacks to a + /// call envelope before consuming nonce/replay state. + pub fn require_principal_action( + &mut self, + expected: Principal, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.authorizations.authorize_principal_action( + expected, + self.context, + authorization, + envelope, + self.now, + ) + } + + /// Verifies a signed authorization and consumes its nonce/replay state. + /// + /// This is a low-level primitive. Public contract methods should usually + /// prefer `require_signed_action` so the intended call envelope is checked + /// before nonce state is consumed. + pub fn require_unbound_signed( + &mut self, + authorization: &SignedAuthorization, + ) -> Principal { + self.authorizations + .authorize_unbound_signed(authorization, self.now) + } + + /// Verifies a signed authorization, applies an authorization predicate, + /// and only then consumes nonce/replay state. + pub fn require_unbound_signed_if( + &mut self, + authorization: &SignedAuthorization, + is_authorized: impl FnOnce(Principal) -> bool, + ) -> Principal { + let principal = + self.authorizations.verify_signed(authorization, self.now); + if !is_authorized(principal) { + panic!("{}", error::UNAUTHORIZED); + } + self.authorizations.consume_verified(authorization); + principal + } + + /// Verifies a signed authorization for an exact call envelope and consumes + /// its nonce/replay state. + pub fn require_signed_action( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + ) -> Principal { + self.authorizations.authorize_signed_action( + authorization, + envelope, + self.now, + ) + } + + /// Verifies a signed authorization for an exact call envelope, applies an + /// authorization predicate, and only then consumes nonce/replay state. + pub fn require_signed_action_if( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + is_authorized: impl FnOnce(Principal) -> bool, + ) -> Principal { + let principal = self.authorizations.verify_signed_action( + authorization, + envelope, + self.now, + ); + if !is_authorized(principal) { + panic!("{}", error::UNAUTHORIZED); + } + self.authorizations.consume_verified(authorization); + principal + } +} + +/// Authorization manager with nonce and replay-key state. +#[derive(Clone, Debug, Default)] +pub struct AuthorizationManager { + nonces: NonceManager, + replays: ReplayGuard, +} + +impl AuthorizationManager { + /// Creates an empty manager. + pub const fn new() -> Self { + Self { + nonces: NonceManager::new(), + replays: ReplayGuard::new(), + } + } + + /// Returns current nonce for a principal/domain stream. + pub fn nonce(&self, principal: Principal, domain: NonceDomain) -> u64 { + self.nonces.current(principal, domain) + } + + /// Returns whether a replay key has been consumed. + pub fn replay_used(&self, principal: Principal, key: ReplayKey) -> bool { + self.replays.is_used(principal, key) + } + + /// Authorizes an exact expected principal. + /// + /// Moonlight and contract principals can be authorized by the runtime call + /// context. Moonlight can also be authorized by a BLS signed action. + /// Phoenix requires a Schnorr signed action because Phoenix does not expose + /// a stable runtime caller identity to contracts. + /// + /// Signed fallbacks are not checked against a call envelope. Prefer + /// [`Self::authorize_principal_action`] in public contract methods unless + /// the caller has already bound the action elsewhere. + pub fn authorize_principal( + &mut self, + expected: Principal, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + match expected.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if context.principal == Some(expected) { + return expected; + } + } + PrincipalKind::Phoenix => {} + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = self.verify_signed(authorization, now); + if principal != expected { + panic!("{}", error::UNAUTHORIZED); + } + self.consume_verified(authorization); + principal + } + + /// Authorizes an exact expected principal and binds signed fallbacks to a + /// call envelope before nonce/replay state is consumed. + pub fn authorize_principal_action( + &mut self, + expected: Principal, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + match expected.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if context.principal == Some(expected) { + return expected; + } + } + PrincipalKind::Phoenix => {} + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = self.verify_signed_action(authorization, envelope, now); + if principal != expected { + panic!("{}", error::UNAUTHORIZED); + } + self.consume_verified(authorization); + principal + } + + /// Verifies and consumes a signed authorization, returning its principal. + /// + /// This is a low-level primitive. Public contract methods should usually + /// prefer `authorize_signed_action` so the intended call envelope is + /// checked before nonce state is consumed. + pub fn authorize_unbound_signed( + &mut self, + authorization: &SignedAuthorization, + now: u64, + ) -> Principal { + let principal = self.verify_signed(authorization, now); + self.consume_verified(authorization); + principal + } + + /// Verifies a signed authorization for an exact call envelope and consumes + /// its nonce/replay state. + pub fn authorize_signed_action( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let principal = self.verify_signed_action(authorization, envelope, now); + self.consume_verified(authorization); + principal + } + + /// Verifies a signed authorization without consuming nonce/replay state. + fn verify_signed( + &self, + authorization: &SignedAuthorization, + now: u64, + ) -> Principal { + match authorization { + SignedAuthorization::Moonlight(auth) => { + self.verify_moonlight(auth, now) + } + SignedAuthorization::Phoenix(auth) => { + self.verify_phoenix(auth, now) + } + } + } + + /// Verifies a signed authorization for an exact call envelope without + /// consuming nonce/replay state. + /// + /// This is an advanced primitive for higher-level authorization policies + /// such as multisig threshold checks. Callers must only consume the + /// authorization after all policy checks have succeeded. + pub fn verify_signed_action( + &self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + authorization.assert_envelope(envelope); + self.verify_signed(authorization, now) + } + + /// Verifies and consumes a Moonlight BLS authorization. + /// + /// The signed action is not checked against a call envelope. Prefer + /// [`Self::authorize_moonlight_action`] in public contract methods. + pub fn authorize_unbound_moonlight( + &mut self, + auth: &MoonlightAuthorization, + now: u64, + ) -> Principal { + let principal = self.verify_moonlight(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies and consumes a Moonlight BLS authorization for an exact call + /// envelope. + pub fn authorize_moonlight_action( + &mut self, + auth: &MoonlightAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + auth.action.assert_envelope(envelope); + let principal = self.verify_moonlight(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies a Moonlight BLS authorization without consuming nonce state. + fn verify_moonlight( + &self, + auth: &MoonlightAuthorization, + now: u64, + ) -> Principal { + assert_live(&auth.action, now); + let principal = Principal::moonlight(&auth.public_key); + if auth.action.principal != principal { + panic!("{}", error::UNAUTHORIZED); + } + let message = auth.action.message_bytes(); + if !verify_bls(&message, &auth.public_key, &auth.signature) { + panic!("{}", error::UNAUTHORIZED); + } + self.assert_nonce(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies and consumes a Phoenix authorization. + /// + /// The signed action is not checked against a call envelope. Prefer + /// [`Self::authorize_phoenix_action`] in public contract methods. + pub fn authorize_unbound_phoenix( + &mut self, + auth: &PhoenixSignatureAuthorization, + now: u64, + ) -> Principal { + let principal = self.verify_phoenix(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + if let Some(key) = auth.replay_key { + self.replays.consume(principal, key); + } + principal + } + + /// Verifies and consumes a Phoenix Schnorr authorization for an exact call + /// envelope. + pub fn authorize_phoenix_action( + &mut self, + auth: &PhoenixSignatureAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + auth.action.assert_envelope(envelope); + let principal = self.verify_phoenix(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + if let Some(key) = auth.replay_key { + self.replays.consume(principal, key); + } + principal + } + + /// Verifies a Phoenix Schnorr authorization without consuming nonce/replay + /// state. + fn verify_phoenix( + &self, + auth: &PhoenixSignatureAuthorization, + now: u64, + ) -> Principal { + assert_live(&auth.action, now); + let principal = Principal::phoenix_public_key(&auth.public_key); + if auth.action.principal != principal { + panic!("{}", error::UNAUTHORIZED); + } + if !verify_schnorr( + auth.action.message_hash(), + &auth.public_key, + &auth.signature, + ) { + panic!("{}", error::UNAUTHORIZED); + } + if let Some(key) = auth.replay_key { + if self.replays.is_used(principal, key) { + panic!("{}", error::REPLAY); + } + } + self.assert_nonce(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Exports nonce streams. + pub fn nonce_entries(&self) -> Vec { + self.nonces.entries() + } + + /// Imports nonce streams, keeping the greatest value for duplicates. + pub fn import_nonce_entries( + &mut self, + entries: impl IntoIterator, + ) { + self.nonces.import_entries(entries); + } + + /// Exports replay keys. + pub fn replay_entries(&self) -> Vec { + self.replays.entries() + } + + /// Imports replay keys. + pub fn import_replay_entries( + &mut self, + entries: impl IntoIterator, + ) { + self.replays.import_entries(entries); + } + + fn assert_nonce( + &self, + principal: Principal, + domain: NonceDomain, + expected: u64, + ) { + if self.nonces.current(principal, domain) != expected { + panic!("{}", error::INVALID_NONCE); + } + } + + /// Consumes nonce/replay state for an authorization that was already + /// verified against its call envelope. + /// + /// This is intentionally low level. Public contract methods should usually + /// use `authorize_signed_action` or a higher-level helper so verification + /// and consumption remain coupled. + pub fn consume_verified(&mut self, authorization: &SignedAuthorization) { + let action = authorization.action(); + self.consume_action(action.principal, action.domain, action.nonce); + if let SignedAuthorization::Phoenix(auth) = authorization { + if let Some(key) = auth.replay_key { + self.replays.consume(action.principal, key); + } + } + } + + fn consume_action( + &mut self, + principal: Principal, + domain: NonceDomain, + nonce: u64, + ) { + self.nonces.consume(principal, domain, nonce); + } +} + +fn assert_live(action: &AuthorizedAction, now: u64) { + if action.is_expired(now) { + panic!("Authorization: expired"); + } +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn hash_message(message: Vec) -> BlsScalar { + dusk_core::abi::hash(message) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn hash_message(message: Vec) -> BlsScalar { + BlsScalar::hash_to_scalar(&message) +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn verify_bls( + message: &[u8], + public_key: &BlsPublicKey, + signature: &BlsSignature, +) -> bool { + dusk_core::abi::verify_bls(message.to_vec(), *public_key, *signature) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn verify_bls( + message: &[u8], + public_key: &BlsPublicKey, + signature: &BlsSignature, +) -> bool { + public_key.verify(signature, message).is_ok() +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn verify_schnorr( + message: BlsScalar, + public_key: &SchnorrPublicKey, + signature: &SchnorrSignature, +) -> bool { + dusk_core::abi::verify_schnorr(message, *public_key, *signature) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn verify_schnorr( + message: BlsScalar, + public_key: &SchnorrPublicKey, + signature: &SchnorrSignature, +) -> bool { + public_key.verify(signature, message).is_ok() +} diff --git a/standards/dusk-contract-standards/src/core/context.rs b/standards/dusk-contract-standards/src/core/context.rs new file mode 100644 index 00000000..7b94b490 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/context.rs @@ -0,0 +1,157 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Runtime call context helpers. + +use crate::core::principal::Principal; + +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::abi::ContractId; +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::signatures::bls::PublicKey as BlsPublicKey; +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::transfer::TRANSFER_CONTRACT; + +/// Current call context. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CallContext { + /// Current actor, if the runtime exposes one. + pub principal: Option, +} + +impl CallContext { + /// Creates a context from an explicit principal. + pub const fn from_principal(principal: Principal) -> Self { + Self { + principal: Some(principal), + } + } + + /// Creates an empty context. + pub const fn none() -> Self { + Self { principal: None } + } + + /// Returns the principal or panics with `msg`. + pub fn require_principal(&self, msg: &str) -> Principal { + self.principal.unwrap_or_else(|| panic!("{}", msg)) + } + + /// Reads the current Dusk runtime context. + /// + /// A root Moonlight transaction routed through the transfer contract is + /// represented by `public_sender`. An inter-contract call is represented + /// by the immediate caller contract id. Phoenix transactions do not expose + /// a stable owner identity here; Phoenix authorization should be modeled + /// with an explicit Schnorr signature plus nonce/replay protection. + #[cfg(all(target_family = "wasm", feature = "contract"))] + pub fn current() -> Self { + use dusk_core::abi; + + let caller = abi::caller(); + let callstack_len = abi::callstack().len(); + let public_sender = match caller { + Some(TRANSFER_CONTRACT) if callstack_len <= 1 => { + abi::public_sender() + } + _ => None, + }; + + Self::from_runtime_parts(caller, public_sender, callstack_len) + } + + /// Native tests must inject context explicitly. + #[cfg(not(all(target_family = "wasm", feature = "contract")))] + pub const fn current() -> Self { + Self::none() + } + + #[cfg(any(all(target_family = "wasm", feature = "contract"), test))] + fn from_runtime_parts( + caller: Option, + public_sender: Option, + callstack_len: usize, + ) -> Self { + match caller { + Some(TRANSFER_CONTRACT) if callstack_len <= 1 => { + public_sender.as_ref().map_or_else(Self::none, |pk| { + Self::from_principal(Principal::moonlight(pk)) + }) + } + Some(caller) => Self::from_principal(Principal::Contract(caller)), + None => Self::none(), + } + } +} + +#[cfg(test)] +mod tests { + use rand::rngs::StdRng; + use rand::SeedableRng; + + use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, + }; + + use super::*; + + fn moonlight_key(seed: u64) -> BlsPublicKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsPublicKey::from(&BlsSecretKey::random(&mut rng)) + } + + fn contract(byte: u8) -> ContractId { + ContractId::from_bytes([byte; 32]) + } + + #[test] + fn transfer_entrypoint_moonlight_uses_public_sender() { + let pk = moonlight_key(1); + let context = CallContext::from_runtime_parts( + Some(TRANSFER_CONTRACT), + Some(pk), + 1, + ); + assert_eq!(context.principal, Some(Principal::moonlight(&pk))); + } + + #[test] + fn transfer_entrypoint_without_public_sender_has_no_actor() { + let context = + CallContext::from_runtime_parts(Some(TRANSFER_CONTRACT), None, 1); + assert_eq!(context.principal, None); + } + + #[test] + fn nested_contract_call_keeps_immediate_contract_caller() { + let pk = moonlight_key(2); + let caller = contract(7); + let context = + CallContext::from_runtime_parts(Some(caller), Some(pk), 2); + assert_eq!(context.principal, Some(Principal::Contract(caller))); + } + + #[test] + fn nested_transfer_call_is_not_rewritten_to_public_sender() { + let pk = moonlight_key(3); + let context = CallContext::from_runtime_parts( + Some(TRANSFER_CONTRACT), + Some(pk), + 2, + ); + assert_eq!( + context.principal, + Some(Principal::Contract(TRANSFER_CONTRACT)) + ); + } + + #[test] + fn no_caller_has_no_observed_actor() { + let pk = moonlight_key(4); + let context = CallContext::from_runtime_parts(None, Some(pk), 0); + assert_eq!(context.principal, None); + } +} diff --git a/standards/dusk-contract-standards/src/core/error.rs b/standards/dusk-contract-standards/src/core/error.rs new file mode 100644 index 00000000..5874cf31 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/error.rs @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Stable error strings used by the reusable primitives. + +pub const ALREADY_INITIALIZED: &str = "DuskStandards: already initialized"; +pub const NOT_INITIALIZED: &str = "DuskStandards: not initialized"; +pub const UNAUTHORIZED: &str = "DuskStandards: unauthorized"; +pub const INVALID_OWNER: &str = "DuskStandards: invalid owner"; +pub const INVALID_PRINCIPAL: &str = "DuskStandards: invalid principal"; +pub const ZERO_PRINCIPAL: &str = "DuskStandards: zero principal"; +pub const REPLAY: &str = "DuskStandards: replay"; +pub const INVALID_NONCE: &str = "DuskStandards: invalid nonce"; +pub const DELAY_NOT_ELAPSED: &str = "DuskStandards: delay not elapsed"; +pub const OPERATION_DONE: &str = "DuskStandards: operation already done"; +pub const OPERATION_UNKNOWN: &str = "DuskStandards: operation unknown"; +pub const INVALID_OPERATION: &str = "DuskStandards: invalid operation"; +pub const OVERFLOW: &str = "DuskStandards: arithmetic overflow"; +pub const UNDERFLOW: &str = "DuskStandards: arithmetic underflow"; diff --git a/standards/dusk-contract-standards/src/core/mod.rs b/standards/dusk-contract-standards/src/core/mod.rs new file mode 100644 index 00000000..14d2602a --- /dev/null +++ b/standards/dusk-contract-standards/src/core/mod.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Shared identity, context, replay, and error primitives. + +pub mod context; +pub mod error; +pub mod nonce; +pub mod principal; +pub mod replay; + +pub use context::CallContext; +pub use nonce::{NonceDomain, NonceEntry, NonceManager, NonceQuery}; +pub use principal::{Principal, PrincipalKind, BLS_PUBLIC_KEY_BYTES}; +pub use replay::{ReplayEntry, ReplayGuard, ReplayKey}; diff --git a/standards/dusk-contract-standards/src/core/nonce.rs b/standards/dusk-contract-standards/src/core/nonce.rs new file mode 100644 index 00000000..d791ccd4 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/nonce.rs @@ -0,0 +1,136 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Monotonic nonce management for replay-protected authorizations. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; + +/// Domain separator for a nonce stream. +/// +/// A contract can keep independent streams for permits, role delegations, +/// Phoenix signature authorizations, upgrade approvals, or any other signed +/// flow. +pub type NonceDomain = [u8; 32]; + +/// Monotonic nonces keyed by `(principal, domain)`. +#[derive(Clone, Debug, Default)] +pub struct NonceManager { + nonces: BTreeMap<(Principal, NonceDomain), u64>, +} + +/// Portable nonce snapshot entry for migration or tests. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceEntry { + /// Principal owning this nonce stream. + pub principal: Principal, + /// Stream domain. + pub domain: NonceDomain, + /// Current nonce. + pub nonce: u64, +} + +/// Nonce query shared by reference contracts and clients. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + /// Principal owning this nonce stream. + pub principal: Principal, + /// Stream domain. + pub domain: NonceDomain, +} + +impl NonceManager { + /// Creates an empty nonce manager. + pub const fn new() -> Self { + Self { + nonces: BTreeMap::new(), + } + } + + /// Returns the current nonce. + pub fn current(&self, principal: Principal, domain: NonceDomain) -> u64 { + self.nonces.get(&(principal, domain)).copied().unwrap_or(0) + } + + /// Consumes exactly `expected` and increments the stream. + pub fn consume( + &mut self, + principal: Principal, + domain: NonceDomain, + expected: u64, + ) -> u64 { + let current = self.current(principal, domain); + if current != expected { + panic!("{}", error::INVALID_NONCE); + } + let next = current.checked_add(1).expect(error::OVERFLOW); + self.nonces.insert((principal, domain), next); + next + } + + /// Consumes the current nonce and returns the consumed value. + pub fn use_next( + &mut self, + principal: Principal, + domain: NonceDomain, + ) -> u64 { + let current = self.current(principal, domain); + self.consume(principal, domain, current); + current + } + + /// Invalidates all nonces below `new_nonce`. + pub fn invalidate_until( + &mut self, + principal: Principal, + domain: NonceDomain, + new_nonce: u64, + ) { + if new_nonce < self.current(principal, domain) { + panic!("{}", error::INVALID_NONCE); + } + self.nonces.insert((principal, domain), new_nonce); + } + + /// Exports all nonce streams. + pub fn entries(&self) -> Vec { + self.nonces + .iter() + .map(|((principal, domain), nonce)| NonceEntry { + principal: *principal, + domain: *domain, + nonce: *nonce, + }) + .collect() + } + + /// Imports nonce streams, keeping the greatest value for duplicates. + pub fn import_entries( + &mut self, + entries: impl IntoIterator, + ) { + for entry in entries { + let current = self.current(entry.principal, entry.domain); + if entry.nonce > current { + self.nonces + .insert((entry.principal, entry.domain), entry.nonce); + } + } + } +} diff --git a/standards/dusk-contract-standards/src/core/principal.rs b/standards/dusk-contract-standards/src/core/principal.rs new file mode 100644 index 00000000..15a531e9 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/principal.rs @@ -0,0 +1,222 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Dusk principal identity. + +use alloc::vec::Vec; +use core::cmp::Ordering; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::PublicKey as BlsPublicKey; +use dusk_core::signatures::schnorr::PublicKey as SchnorrPublicKey; +use dusk_core::JubJubAffine; +use rkyv::{Archive, Deserialize, Serialize}; + +/// Raw byte length of a Dusk Moonlight BLS public key. +pub const BLS_PUBLIC_KEY_BYTES: usize = 193; + +/// Coarse principal kind for policy decisions and event indexing. +#[derive( + Archive, + Serialize, + Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum PrincipalKind { + /// Transparent Moonlight public account. + Moonlight, + /// Privacy-preserving Phoenix authorization identity. + Phoenix, + /// Dusk contract id. + Contract, +} + +/// An actor that can own assets, hold roles, or authorize contract actions. +/// +/// Phoenix identity is represented as compressed Schnorr public-key bytes. The +/// primitive deliberately does not pretend that a Phoenix transaction exposes +/// an Ethereum-like caller address. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +pub enum Principal { + /// Transparent Moonlight public account, stored as raw BLS public-key + /// bytes. + Moonlight([u8; BLS_PUBLIC_KEY_BYTES]), + /// Phoenix Schnorr authorization identity. + Phoenix([u8; 32]), + /// Contract account. + Contract(ContractId), +} + +impl Principal { + /// Returns the principal kind. + pub const fn kind(&self) -> PrincipalKind { + match self { + Self::Moonlight(_) => PrincipalKind::Moonlight, + Self::Phoenix(_) => PrincipalKind::Phoenix, + Self::Contract(_) => PrincipalKind::Contract, + } + } + + /// Constructs a Moonlight principal from a public key. + pub fn moonlight(pk: &BlsPublicKey) -> Self { + Self::Moonlight(pk.to_raw_bytes()) + } + + /// Constructs a contract principal from a contract id. + pub const fn contract(id: ContractId) -> Self { + Self::Contract(id) + } + + /// Constructs a Phoenix principal from raw compressed public-key bytes. + pub const fn phoenix(public_key_bytes: [u8; 32]) -> Self { + Self::Phoenix(public_key_bytes) + } + + /// Constructs a Phoenix principal from a Schnorr public key. + pub fn phoenix_public_key(pk: &SchnorrPublicKey) -> Self { + Self::Phoenix(JubJubAffine::from(pk.as_ref()).to_bytes()) + } + + /// Returns true when this is the reserved all-zero principal value. + pub fn is_zero(&self) -> bool { + match self { + Self::Moonlight(bytes) => bytes.iter().all(|byte| *byte == 0), + Self::Phoenix(bytes) => bytes.iter().all(|byte| *byte == 0), + Self::Contract(id) => id.to_bytes().iter().all(|byte| *byte == 0), + } + } + + /// Stable byte representation used for hashing and replay keys. + pub fn to_bytes(&self) -> Vec { + let mut out = Vec::new(); + out.push(match self { + Self::Moonlight(_) => 0, + Self::Phoenix(_) => 1, + Self::Contract(_) => 2, + }); + match self { + Self::Moonlight(bytes) => out.extend_from_slice(bytes), + Self::Phoenix(bytes) => out.extend_from_slice(bytes), + Self::Contract(id) => out.extend_from_slice(&id.to_bytes()), + } + out + } +} + +impl From for Principal { + fn from(value: BlsPublicKey) -> Self { + Self::moonlight(&value) + } +} + +impl From for Principal { + fn from(value: ContractId) -> Self { + Self::Contract(value) + } +} + +impl PartialOrd for Principal { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Principal { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Moonlight(lhs), Self::Moonlight(rhs)) => lhs.cmp(rhs), + (Self::Phoenix(lhs), Self::Phoenix(rhs)) => lhs.cmp(rhs), + (Self::Contract(lhs), Self::Contract(rhs)) => lhs.cmp(rhs), + _ => self.kind().cmp(&other.kind()), + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Principal { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("Principal", 2)?; + state.serialize_field("kind", &self.kind())?; + match self { + Self::Moonlight(bytes) => { + state.serialize_field("bytes", &bytes.as_slice())? + } + Self::Phoenix(bytes) => { + state.serialize_field("bytes", &bytes.as_slice())? + } + Self::Contract(id) => { + state.serialize_field("bytes", &id.to_bytes().as_slice())? + } + } + state.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Principal { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct PrincipalJson { + kind: PrincipalKind, + bytes: Vec, + } + + let principal = + ::deserialize(deserializer)?; + match principal.kind { + PrincipalKind::Moonlight => { + let bytes: [u8; BLS_PUBLIC_KEY_BYTES] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"193 Moonlight public-key bytes", + ) + })?; + Ok(Self::Moonlight(bytes)) + } + PrincipalKind::Phoenix => { + let bytes: [u8; 32] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"32 Phoenix public-key bytes", + ) + })?; + Ok(Self::Phoenix(bytes)) + } + PrincipalKind::Contract => { + let bytes: [u8; 32] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"32 contract-id bytes", + ) + })?; + Ok(Self::Contract(ContractId::from_bytes(bytes))) + } + } + } +} diff --git a/standards/dusk-contract-standards/src/core/replay.rs b/standards/dusk-contract-standards/src/core/replay.rs new file mode 100644 index 00000000..c2edcbb0 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/replay.rs @@ -0,0 +1,80 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Replay protection for signed authorizations. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::error; +use crate::core::principal::Principal; + +/// Replay key supplied by an authorization flow. +pub type ReplayKey = [u8; 32]; + +/// Tracks consumed replay keys per principal. +#[derive(Clone, Debug, Default)] +pub struct ReplayGuard { + used: BTreeSet<(Principal, ReplayKey)>, +} + +/// Portable snapshot entry for migration or tests. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ReplayEntry { + /// Principal that consumed the key. + pub principal: Principal, + /// Consumed key. + pub key: ReplayKey, +} + +impl ReplayGuard { + /// Creates an empty replay guard. + pub const fn new() -> Self { + Self { + used: BTreeSet::new(), + } + } + + /// Returns whether a key has been consumed. + pub fn is_used(&self, principal: Principal, key: ReplayKey) -> bool { + self.used.contains(&(principal, key)) + } + + /// Consumes a replay key or panics when it was already used. + pub fn consume(&mut self, principal: Principal, key: ReplayKey) { + if !self.used.insert((principal, key)) { + panic!("{}", error::REPLAY); + } + } + + /// Exports all entries for migration or inspection. + pub fn entries(&self) -> Vec { + self.used + .iter() + .map(|(principal, key)| ReplayEntry { + principal: *principal, + key: *key, + }) + .collect() + } + + /// Imports entries, rejecting conflicting duplicates through the set. + pub fn import_entries( + &mut self, + entries: impl IntoIterator, + ) { + for entry in entries { + self.used.insert((entry.principal, entry.key)); + } + } +} diff --git a/standards/dusk-contract-standards/src/governance/controller.rs b/standards/dusk-contract-standards/src/governance/controller.rs new file mode 100644 index 00000000..ed99fabf --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/controller.rs @@ -0,0 +1,200 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Role-gated timelock controller. + +use alloc::vec::Vec; + +use crate::access::{AccessControl, Role}; +use crate::core::{error, Principal}; +use crate::governance::{OperationId, ScheduledOperation, Timelock}; + +/// Role allowed to update timelock policy. +pub const TIMELOCK_ADMIN_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 1; + role +}; + +/// Role allowed to schedule operations. +pub const PROPOSER_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 2; + role +}; + +/// Role allowed to execute ready operations. +pub const EXECUTOR_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 3; + role +}; + +/// Role allowed to cancel pending operations. +pub const CANCELLER_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 4; + role +}; + +/// Timelock plus role-based caller policy. +#[derive(Clone, Debug)] +pub struct TimelockController { + access: AccessControl, + timelock: Timelock, + self_principal: Principal, +} + +impl TimelockController { + /// Creates a controller and grants all controller roles to `admin`. + pub fn new( + self_principal: Principal, + admin: Principal, + min_delay: u64, + ) -> Self { + if self_principal.is_zero() || admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut access = AccessControl::new(); + access.init_admin(admin); + access.grant_role(admin, TIMELOCK_ADMIN_ROLE, admin); + access.grant_role(admin, PROPOSER_ROLE, admin); + access.grant_role(admin, EXECUTOR_ROLE, admin); + access.grant_role(admin, CANCELLER_ROLE, admin); + Self { + access, + timelock: Timelock::new(min_delay), + self_principal, + } + } + + /// Returns the controller principal used for self-governed operations. + pub const fn self_principal(&self) -> Principal { + self.self_principal + } + + /// Returns access-control state. + pub const fn access(&self) -> &AccessControl { + &self.access + } + + /// Returns timelock state. + pub const fn timelock(&self) -> &Timelock { + &self.timelock + } + + /// Returns a scheduled operation. + pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> { + self.timelock.get(id) + } + + /// Returns whether an account has a role. + pub fn has_role(&self, role: Role, account: Principal) -> bool { + self.access.has_role(role, account) + } + + /// Grants a role through the underlying access-control policy. + pub fn grant_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + self.access.grant_role(caller, role, account); + } + + /// Revokes a role through the underlying access-control policy. + pub fn revoke_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + self.access.revoke_role(caller, role, account); + } + + /// Updates the minimum delay. Caller must be the controller itself. + /// + /// Composing contracts should reach this path through a scheduled + /// operation, not through an arbitrary administrator call. + pub fn set_min_delay(&mut self, caller: Principal, min_delay: u64) { + if caller != self.self_principal { + panic!("{}", error::UNAUTHORIZED); + } + self.timelock.set_min_delay(min_delay); + } + + /// Schedules a self-governed minimum-delay update. + pub fn schedule_min_delay_change( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + min_delay: u64, + ) -> u64 { + self.access.assert_role(PROPOSER_ROLE, caller); + self.timelock + .schedule(id, now, min_delay.to_be_bytes().to_vec()) + } + + /// Executes a ready minimum-delay update as the controller itself. + pub fn execute_min_delay_change( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + ) -> u64 { + self.access.assert_role(EXECUTOR_ROLE, caller); + let op = self + .timelock + .get(id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if op.done { + panic!("{}", error::OPERATION_DONE); + } + if now < op.ready_at { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + let bytes: [u8; 8] = op + .payload + .as_slice() + .try_into() + .unwrap_or_else(|_| panic!("{}", error::INVALID_OPERATION)); + let min_delay = u64::from_be_bytes(bytes); + self.timelock.execute(id, now); + self.set_min_delay(self.self_principal, min_delay); + min_delay + } + + /// Schedules an operation. Caller must have proposer role. + pub fn schedule( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + payload: Vec, + ) -> u64 { + self.access.assert_role(PROPOSER_ROLE, caller); + self.timelock.schedule(id, now, payload) + } + + /// Cancels a pending operation. Caller must have canceller role. + pub fn cancel(&mut self, caller: Principal, id: OperationId) { + self.access.assert_role(CANCELLER_ROLE, caller); + self.timelock.cancel(id); + } + + /// Executes a ready operation. Caller must have executor role. + pub fn execute( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + ) -> Vec { + self.access.assert_role(EXECUTOR_ROLE, caller); + self.timelock.execute(id, now) + } +} diff --git a/standards/dusk-contract-standards/src/governance/mod.rs b/standards/dusk-contract-standards/src/governance/mod.rs new file mode 100644 index 00000000..9f0cae8b --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/mod.rs @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Governance and scheduling primitives. + +pub mod controller; +pub mod multisig; +pub mod multisig_controller; +pub mod timelock; + +pub use controller::{ + TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, + TIMELOCK_ADMIN_ROLE, +}; +pub use multisig::{ + MultisigApprovals, MultisigConfig, Threshold, ThresholdMultisig, +}; +pub use multisig_controller::{ + MultisigAuthorityUpdated, MultisigController, MultisigControllerConfig, + MultisigControllerOutcome, MultisigControllerStatus, + MultisigOperationCancelled, MultisigOperationConfirmed, + MultisigOperationExecuted, MultisigOperationId, MultisigOperationProposed, + MultisigPendingOperation, MultisigTarget, MultisigTimeLimitsUpdated, + MULTISIG_AUTHORITY_UPDATED_TOPIC, MULTISIG_OPERATION_CANCELLED_TOPIC, + MULTISIG_OPERATION_CONFIRMED_TOPIC, MULTISIG_OPERATION_EXECUTED_TOPIC, + MULTISIG_OPERATION_PROPOSED_TOPIC, MULTISIG_TIME_LIMITS_UPDATED_TOPIC, +}; +pub use timelock::{OperationId, ScheduledOperation, Timelock}; diff --git a/standards/dusk-contract-standards/src/governance/multisig.rs b/standards/dusk-contract-standards/src/governance/multisig.rs new file mode 100644 index 00000000..6ba2f88c --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/multisig.rs @@ -0,0 +1,266 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Threshold multisig authorization for Dusk principals. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ActionEnvelope, AuthorizationManager, SignedAuthorization}; +use crate::core::{error, CallContext, Principal, PrincipalKind}; + +/// Owner threshold type used by multisig policies. +pub type Threshold = u16; + +/// Initialization config for a threshold multisig policy. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigConfig { + /// Unique non-zero owners. + pub owners: Vec, + /// Required number of distinct owners. + pub threshold: Threshold, +} + +/// Signed approvals submitted with a multisig-gated call. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigApprovals { + /// Signed Moonlight/Phoenix approvals for the exact call envelope. + pub approvals: Vec, +} + +/// Dusk-native threshold multisig authorization policy. +/// +/// The primitive is intentionally an action authorizer, not an EVM-style +/// arbitrary calldata executor. Composing contracts bind approvals to their own +/// `ActionEnvelope`, then execute the local state transition after this module +/// returns a threshold of distinct owner signers. +#[derive(Clone, Debug, Default)] +pub struct ThresholdMultisig { + owners: BTreeSet, + threshold: Threshold, +} + +impl ThresholdMultisig { + /// Creates an uninitialized multisig policy. + pub const fn new() -> Self { + Self { + owners: BTreeSet::new(), + threshold: 0, + } + } + + /// Initializes the policy with unique owners and a non-zero threshold. + pub fn init(&mut self, config: MultisigConfig) { + if self.threshold != 0 || !self.owners.is_empty() { + panic!("{}", error::ALREADY_INITIALIZED); + } + let owners = collect_owners(config.owners); + validate_threshold(config.threshold, owners.len()); + self.owners = owners; + self.threshold = config.threshold; + } + + /// Returns all owners in stable order. + pub fn owners(&self) -> Vec { + self.owners.iter().copied().collect() + } + + /// Returns the number of owners. + pub fn len(&self) -> usize { + self.owners.len() + } + + /// Returns true when there are no owners configured. + pub fn is_empty(&self) -> bool { + self.owners.is_empty() + } + + /// Returns the current threshold. + pub const fn threshold(&self) -> Threshold { + self.threshold + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.owners.contains(&principal) + } + + /// Verifies that the observed caller plus supplied signed approvals satisfy + /// the threshold for a concrete call envelope. + /// + /// The method verifies every signed approval and all policy constraints + /// before consuming any nonce/replay state. Rejected threshold, duplicate, + /// unauthorized, expired, wrong-envelope, or bad-signature cases are + /// therefore atomic with respect to signer nonces. + pub fn authorize_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + let signers = self.verify_action( + authorizations, + context, + approvals, + envelope, + now, + ); + for approval in approvals { + authorizations.consume_verified(approval); + } + signers + } + + /// Verifies that the observed caller plus supplied signed approvals satisfy + /// the threshold for a concrete call envelope without consuming nonce or + /// replay state. + pub fn verify_action( + &self, + authorizations: &AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.assert_initialized(); + + let mut signers = BTreeSet::new(); + if let Some(principal) = context.principal { + match principal.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if self.is_owner(principal) { + signers.insert(principal); + } + } + PrincipalKind::Phoenix => {} + } + } + + for approval in approvals { + let principal = + authorizations.verify_signed_action(approval, envelope, now); + if !self.is_owner(principal) || !signers.insert(principal) { + panic!("{}", error::UNAUTHORIZED); + } + } + + self.assert_threshold_count(signers.len()); + signers.iter().copied().collect() + } + + /// Panics unless `signers` are a distinct threshold of current owners. + pub fn assert_quorum(&self, signers: &[Principal]) { + self.assert_initialized(); + let mut unique = BTreeSet::new(); + for signer in signers { + if !self.is_owner(*signer) || !unique.insert(*signer) { + panic!("{}", error::UNAUTHORIZED); + } + } + self.assert_threshold_count(unique.len()); + } + + /// Adds an owner after a threshold of current owners has authorized it. + pub fn add_owner(&mut self, signers: &[Principal], owner: Principal) { + self.assert_quorum(signers); + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !self.owners.insert(owner) { + panic!("{}", error::INVALID_OWNER); + } + } + + /// Removes an owner and sets the threshold that should apply after removal. + pub fn remove_owner( + &mut self, + signers: &[Principal], + owner: Principal, + new_threshold: Threshold, + ) { + self.assert_quorum(signers); + let mut owners = self.owners.clone(); + if !owners.remove(&owner) { + panic!("{}", error::INVALID_OWNER); + } + validate_threshold(new_threshold, owners.len()); + self.owners = owners; + self.threshold = new_threshold; + } + + /// Replaces one owner with another after threshold authorization. + pub fn replace_owner( + &mut self, + signers: &[Principal], + old_owner: Principal, + new_owner: Principal, + ) { + self.assert_quorum(signers); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut owners = self.owners.clone(); + if !owners.remove(&old_owner) || !owners.insert(new_owner) { + panic!("{}", error::INVALID_OWNER); + } + self.owners = owners; + } + + /// Changes the threshold after a threshold of current owners has authorized + /// it. + pub fn set_threshold( + &mut self, + signers: &[Principal], + threshold: Threshold, + ) { + self.assert_quorum(signers); + validate_threshold(threshold, self.owners.len()); + self.threshold = threshold; + } + + fn assert_initialized(&self) { + if self.threshold == 0 || self.owners.is_empty() { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn assert_threshold_count(&self, count: usize) { + if count < usize::from(self.threshold) { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +fn collect_owners(owners: Vec) -> BTreeSet { + let mut next = BTreeSet::new(); + for owner in owners { + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !next.insert(owner) { + panic!("{}", error::INVALID_OWNER); + } + } + if next.is_empty() { + panic!("{}", error::INVALID_OWNER); + } + next +} + +fn validate_threshold(threshold: Threshold, owner_count: usize) { + if threshold == 0 || usize::from(threshold) > owner_count { + panic!("{}", error::INVALID_OWNER); + } +} diff --git a/standards/dusk-contract-standards/src/governance/multisig_controller.rs b/standards/dusk-contract-standards/src/governance/multisig_controller.rs new file mode 100644 index 00000000..688c9026 --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/multisig_controller.rs @@ -0,0 +1,632 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Standalone multisig controller state machine. + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::mem; + +use bytecheck::CheckBytes; +use dusk_core::transfer::data::ContractCall; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ActionEnvelope, AuthorizationManager, SignedAuthorization}; +use crate::core::{error, CallContext, Principal}; +use crate::governance::{MultisigConfig, Threshold, ThresholdMultisig}; + +/// Multisig operation id. +pub type MultisigOperationId = [u8; 32]; + +/// Event topic emitted when an operation is proposed. +pub const MULTISIG_OPERATION_PROPOSED_TOPIC: &str = + "multisig/operation_proposed"; +/// Event topic emitted when an operation is confirmed. +pub const MULTISIG_OPERATION_CONFIRMED_TOPIC: &str = + "multisig/operation_confirmed"; +/// Event topic emitted when an operation execution is attempted. +pub const MULTISIG_OPERATION_EXECUTED_TOPIC: &str = + "multisig/operation_executed"; +/// Event topic emitted when a pending operation is cancelled. +pub const MULTISIG_OPERATION_CANCELLED_TOPIC: &str = + "multisig/operation_cancelled"; +/// Event topic emitted when owner authority changes. +pub const MULTISIG_AUTHORITY_UPDATED_TOPIC: &str = "multisig/authority_updated"; +/// Event topic emitted when proposal/replay timing changes. +pub const MULTISIG_TIME_LIMITS_UPDATED_TOPIC: &str = + "multisig/time_limits_updated"; + +/// Target call controlled by the multisig. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigTarget { + /// Call executed by the controller after quorum. + pub call: ContractCall, + /// Salt used to intentionally repeat the same logical call. + pub salt: [u8; 32], +} + +/// Initialization config for a standalone multisig controller. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigControllerConfig { + /// Unique non-zero owner principals. + pub owners: Vec, + /// Required distinct confirmations. + pub threshold: Threshold, + /// Number of blocks/heights a proposal remains confirmable. + pub proposal_ttl: u64, + /// Number of blocks/heights an executed operation id remains tombstoned. + pub tombstone_ttl: u64, +} + +/// Pending operation metadata. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigPendingOperation { + /// Target call. + pub target: MultisigTarget, + /// Distinct owner confirmations in stable insertion order. + pub confirmations: Vec, + /// Last block/height at which confirmations are accepted. + pub deadline: u64, +} + +impl MultisigPendingOperation { + /// Returns true when `principal` has already confirmed. + pub fn confirmed_by(&self, principal: Principal) -> bool { + self.confirmations.contains(&principal) + } +} + +/// Lifecycle status for a proposal/confirmation call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum MultisigControllerStatus { + /// A new operation was created. + Proposed, + /// An existing operation received one confirmation. + Confirmed, + /// The operation reached threshold and should be executed by the wrapper. + Ready, +} + +/// Result of proposing or confirming an operation. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigControllerOutcome { + /// Operation id. + pub id: MultisigOperationId, + /// Owner principal that authorized this confirmation. + pub authorizer: Principal, + /// Lifecycle status. + pub status: MultisigControllerStatus, + /// Confirmation count after this call. + pub confirmations: u16, + /// Current threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, + /// Operation to execute when status is + /// [`MultisigControllerStatus::Ready`]. + pub ready_operation: Option, +} + +/// Event payload for proposal creation. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationProposed { + /// Operation id. + pub id: MultisigOperationId, + /// Owner that proposed the operation. + pub authorizer: Principal, + /// Confirmation count after proposal. + pub confirmations: u16, + /// Required confirmation threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, +} + +/// Event payload for operation confirmation. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationConfirmed { + /// Operation id. + pub id: MultisigOperationId, + /// Owner that confirmed the operation. + pub authorizer: Principal, + /// Confirmation count after confirmation. + pub confirmations: u16, + /// Required confirmation threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, +} + +/// Event payload for operation execution attempts. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationExecuted { + /// Operation id. + pub id: MultisigOperationId, + /// Whether the target call returned successfully. + pub success: bool, + /// Raw return data when the target call succeeds. + pub return_data: Vec, + /// Error string when the target call fails. + pub error: Option, +} + +/// Event payload for threshold-cancelled operations. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationCancelled { + /// Operation id. + pub id: MultisigOperationId, + /// Owners that authorized cancellation. + pub signers: Vec, +} + +/// Event payload for authority changes. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigAuthorityUpdated { + /// Previous owners. + pub previous_owners: Vec, + /// Previous threshold. + pub previous_threshold: Threshold, + /// New owners. + pub owners: Vec, + /// New threshold. + pub threshold: Threshold, + /// Pending operations removed because the authority changed. + pub removed_operations: Vec, +} + +/// Event payload for proposal/replay timing changes. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigTimeLimitsUpdated { + /// Previous proposal TTL. + pub previous_proposal_ttl: u64, + /// Previous tombstone TTL. + pub previous_tombstone_ttl: u64, + /// New proposal TTL. + pub proposal_ttl: u64, + /// New tombstone TTL. + pub tombstone_ttl: u64, +} + +impl dusk_forge::ContractEvent for MultisigOperationProposed { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_PROPOSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationConfirmed { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_CONFIRMED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationExecuted { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_EXECUTED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationCancelled { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_CANCELLED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigAuthorityUpdated { + const TOPICS: &'static [&'static str] = &[MULTISIG_AUTHORITY_UPDATED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigTimeLimitsUpdated { + const TOPICS: &'static [&'static str] = + &[MULTISIG_TIME_LIMITS_UPDATED_TOPIC]; +} + +/// Standalone multisig controller primitive. +/// +/// This state machine tracks operation proposals and confirmations. It does +/// not call target contracts itself; Forge wrappers perform the runtime call +/// after this primitive returns a ready operation. +#[derive(Clone, Debug)] +pub struct MultisigController { + policy: ThresholdMultisig, + proposal_ttl: u64, + tombstone_ttl: u64, + proposals: BTreeMap, + tombstones: BTreeMap, +} + +impl MultisigController { + /// Creates an uninitialized controller. + pub const fn new() -> Self { + Self { + policy: ThresholdMultisig::new(), + proposal_ttl: 0, + tombstone_ttl: 0, + proposals: BTreeMap::new(), + tombstones: BTreeMap::new(), + } + } + + /// Initializes the controller. + pub fn init(&mut self, config: MultisigControllerConfig) { + if self.is_initialized() { + panic!("{}", error::ALREADY_INITIALIZED); + } + validate_time_limits(config.proposal_ttl, config.tombstone_ttl); + self.policy.init(MultisigConfig { + owners: config.owners, + threshold: config.threshold, + }); + self.proposal_ttl = config.proposal_ttl; + self.tombstone_ttl = config.tombstone_ttl; + } + + /// Returns true when the controller is initialized. + pub fn is_initialized(&self) -> bool { + !self.policy.is_empty() && self.policy.threshold() != 0 + } + + /// Returns owners in stable order. + pub fn owners(&self) -> Vec { + self.policy.owners() + } + + /// Returns the current threshold. + pub const fn threshold(&self) -> Threshold { + self.policy.threshold() + } + + /// Returns the proposal TTL. + pub const fn proposal_ttl(&self) -> u64 { + self.proposal_ttl + } + + /// Returns the tombstone TTL. + pub const fn tombstone_ttl(&self) -> u64 { + self.tombstone_ttl + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.policy.is_owner(principal) + } + + /// Returns a pending proposal. + pub fn proposal( + &self, + id: MultisigOperationId, + ) -> Option { + self.proposals.get(&id).cloned() + } + + /// Returns a tombstone expiry. + pub fn tombstone_expiry(&self, id: MultisigOperationId) -> Option { + self.tombstones.get(&id).copied() + } + + /// Verifies an action-bound threshold of owner approvals. + pub fn authorize_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.policy.authorize_action( + authorizations, + context, + approvals, + envelope, + now, + ) + } + + /// Verifies an action-bound threshold of owner approvals without consuming + /// nonce or replay state. + pub fn verify_action( + &self, + authorizations: &AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.policy.verify_action( + authorizations, + context, + approvals, + envelope, + now, + ) + } + + /// Proposes an operation and records the first confirmation. + pub fn propose( + &mut self, + id: MultisigOperationId, + target: MultisigTarget, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + self.assert_initialized(); + self.assert_owner(authorizer); + validate_target(&target); + self.prune(now); + self.assert_not_tombstoned(id); + + if self.proposals.contains_key(&id) { + return self.confirm_pending(id, authorizer, now); + } + + let deadline = + now.checked_add(self.proposal_ttl).expect(error::OVERFLOW); + self.proposals.insert( + id, + MultisigPendingOperation { + target, + confirmations: vec![authorizer], + deadline, + }, + ); + + self.finish_if_ready( + id, + authorizer, + MultisigControllerStatus::Proposed, + deadline, + now, + ) + } + + /// Confirms a pending operation. + pub fn confirm( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + self.assert_initialized(); + self.assert_owner(authorizer); + self.prune(now); + self.assert_not_tombstoned(id); + self.confirm_pending(id, authorizer, now) + } + + /// Cancels a pending operation after a current owner quorum authorizes it. + pub fn cancel( + &mut self, + id: MultisigOperationId, + signers: &[Principal], + now: u64, + ) -> MultisigOperationCancelled { + self.assert_initialized(); + self.policy.assert_quorum(signers); + self.prune(now); + if self.proposals.remove(&id).is_none() { + panic!("{}", error::OPERATION_UNKNOWN); + } + MultisigOperationCancelled { + id, + signers: signers.to_vec(), + } + } + + /// Updates owners and threshold after a current owner quorum authorizes it. + /// + /// Pending operations are removed when an owner is removed or the threshold + /// changes, because old confirmations may no longer represent the new + /// authority. + pub fn update_authority( + &mut self, + signers: &[Principal], + owners: Vec, + threshold: Threshold, + ) -> MultisigAuthorityUpdated { + self.assert_initialized(); + self.policy.assert_quorum(signers); + + let previous_owners = self.owners(); + let previous_threshold = self.threshold(); + let mut next = ThresholdMultisig::new(); + next.init(MultisigConfig { owners, threshold }); + let next_owners = next.owners(); + + let removed_owner = + previous_owners.iter().any(|owner| !next.is_owner(*owner)); + let threshold_changed = previous_threshold != next.threshold(); + let removed_operations = if removed_owner || threshold_changed { + self.clear_pending() + } else { + Vec::new() + }; + + self.policy = next; + + MultisigAuthorityUpdated { + previous_owners, + previous_threshold, + owners: next_owners, + threshold, + removed_operations, + } + } + + /// Updates time limits after a current owner quorum authorizes it. + pub fn set_time_limits( + &mut self, + signers: &[Principal], + proposal_ttl: u64, + tombstone_ttl: u64, + ) -> MultisigTimeLimitsUpdated { + self.assert_initialized(); + self.policy.assert_quorum(signers); + validate_time_limits(proposal_ttl, tombstone_ttl); + + let event = MultisigTimeLimitsUpdated { + previous_proposal_ttl: self.proposal_ttl, + previous_tombstone_ttl: self.tombstone_ttl, + proposal_ttl, + tombstone_ttl, + }; + self.proposal_ttl = proposal_ttl; + self.tombstone_ttl = tombstone_ttl; + event + } + + fn confirm_pending( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + let deadline = { + let operation = self + .proposals + .get_mut(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if now > operation.deadline { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + if operation.confirmed_by(authorizer) { + panic!("{}", error::UNAUTHORIZED); + } + operation.confirmations.push(authorizer); + operation.deadline + }; + + self.finish_if_ready( + id, + authorizer, + MultisigControllerStatus::Confirmed, + deadline, + now, + ) + } + + fn finish_if_ready( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + status: MultisigControllerStatus, + deadline: u64, + now: u64, + ) -> MultisigControllerOutcome { + let confirmations = self + .proposals + .get(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)) + .confirmations + .len(); + let mut status = status; + let ready_operation = + if confirmations >= usize::from(self.policy.threshold()) { + status = MultisigControllerStatus::Ready; + let operation = self + .proposals + .remove(&id) + .expect("ready operation must be pending"); + self.tombstones.insert( + id, + now.checked_add(self.tombstone_ttl).expect(error::OVERFLOW), + ); + Some(operation) + } else { + None + }; + + MultisigControllerOutcome { + id, + authorizer, + status, + confirmations: confirmations as u16, + threshold: self.policy.threshold(), + deadline, + ready_operation, + } + } + + fn assert_initialized(&self) { + if !self.is_initialized() { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn assert_owner(&self, authorizer: Principal) { + if !self.policy.is_owner(authorizer) { + panic!("{}", error::UNAUTHORIZED); + } + } + + fn assert_not_tombstoned(&self, id: MultisigOperationId) { + if self.tombstones.contains_key(&id) { + panic!("{}", error::REPLAY); + } + } + + fn prune(&mut self, now: u64) { + self.proposals + .retain(|_, operation| now <= operation.deadline); + self.tombstones.retain(|_, expiry| now <= *expiry); + } + + fn clear_pending(&mut self) -> Vec { + let proposals = mem::take(&mut self.proposals); + proposals.into_keys().collect() + } +} + +impl Default for MultisigController { + fn default() -> Self { + Self::new() + } +} + +fn validate_time_limits(proposal_ttl: u64, tombstone_ttl: u64) { + if proposal_ttl == 0 || tombstone_ttl == 0 { + panic!("{}", error::INVALID_OPERATION); + } +} + +fn validate_target(target: &MultisigTarget) { + if target + .call + .contract + .to_bytes() + .iter() + .all(|byte| *byte == 0) + || target.call.fn_name.is_empty() + { + panic!("{}", error::INVALID_OPERATION); + } +} diff --git a/standards/dusk-contract-standards/src/governance/timelock.rs b/standards/dusk-contract-standards/src/governance/timelock.rs new file mode 100644 index 00000000..3fb6bb5b --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/timelock.rs @@ -0,0 +1,127 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Timelock operation scheduler. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::error; + +/// Operation id. +pub type OperationId = [u8; 32]; + +/// Scheduled operation metadata. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ScheduledOperation { + /// Earliest executable height or timestamp unit selected by the contract. + pub ready_at: u64, + /// Whether the operation has already been executed. + pub done: bool, + /// Optional operation payload for migration or client inspection. + pub payload: Vec, +} + +/// Minimal timelock state machine. +#[derive(Clone, Debug)] +pub struct Timelock { + min_delay: u64, + operations: BTreeMap, +} + +impl Timelock { + /// Creates a timelock with `min_delay`. + pub const fn new(min_delay: u64) -> Self { + Self { + min_delay, + operations: BTreeMap::new(), + } + } + + /// Returns the configured minimum delay. + pub const fn min_delay(&self) -> u64 { + self.min_delay + } + + /// Updates the delay. Caller authorization belongs in the composing + /// contract. + pub fn set_min_delay(&mut self, min_delay: u64) { + self.min_delay = min_delay; + } + + /// Schedules an operation. + pub fn schedule( + &mut self, + id: OperationId, + now: u64, + payload: Vec, + ) -> u64 { + if self.operations.contains_key(&id) { + panic!("{}", error::ALREADY_INITIALIZED); + } + let ready_at = now.checked_add(self.min_delay).expect(error::OVERFLOW); + self.operations.insert( + id, + ScheduledOperation { + ready_at, + done: false, + payload, + }, + ); + ready_at + } + + /// Cancels a pending operation. + pub fn cancel(&mut self, id: OperationId) { + let Some(op) = self.operations.get(&id) else { + panic!("{}", error::OPERATION_UNKNOWN); + }; + if op.done { + panic!("{}", error::OPERATION_DONE); + } + self.operations.remove(&id); + } + + /// Marks an operation as executed and returns its payload. + pub fn execute(&mut self, id: OperationId, now: u64) -> Vec { + let op = self + .operations + .get_mut(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if op.done { + panic!("{}", error::OPERATION_DONE); + } + if now < op.ready_at { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + op.done = true; + op.payload.clone() + } + + /// Returns an operation. + pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> { + self.operations.get(&id) + } + + /// Returns true when operation is ready and not done. + pub fn is_ready(&self, id: OperationId, now: u64) -> bool { + self.operations + .get(&id) + .map(|op| !op.done && now >= op.ready_at) + .unwrap_or(false) + } +} + +impl Default for Timelock { + fn default() -> Self { + Self::new(0) + } +} diff --git a/standards/dusk-contract-standards/src/lib.rs b/standards/dusk-contract-standards/src/lib.rs new file mode 100644 index 00000000..7ca320a9 --- /dev/null +++ b/standards/dusk-contract-standards/src/lib.rs @@ -0,0 +1,42 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Dusk-native reusable contract standards and examples. +//! +//! This crate intentionally does not provide EVM API compatibility. It ports +//! the security concepts into Dusk's account, contract, and call model. + +#![no_std] +#![cfg_attr( + not(all(target_family = "wasm", feature = "contract")), + deny(unused_crate_dependencies) +)] +#![deny(unused_extern_crates)] + +extern crate alloc; + +#[cfg(test)] +use dusk_data_driver as _; +#[cfg(test)] +use dusk_vm as _; +#[cfg(test)] +use proptest as _; +#[cfg(test)] +use rand as _; +#[cfg(test)] +use serde_json as _; +#[cfg(feature = "serde")] +use serde_with as _; +#[cfg(feature = "serde")] +use time as _; + +pub mod access; +pub mod auth; +pub mod core; +pub mod governance; +pub mod proxy; +pub mod security; +pub mod token; diff --git a/standards/dusk-contract-standards/src/proxy/mod.rs b/standards/dusk-contract-standards/src/proxy/mod.rs new file mode 100644 index 00000000..2f478a8c --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/mod.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Dusk-native proxy and upgrade primitives. + +pub mod state_store; +pub mod upgrade; + +pub use state_store::StateStore; +pub use upgrade::{ + PendingUpgrade, RollbackFinalized, UpgradeActivated, UpgradeAdmin, + UpgradeCancelled, UpgradePrepared, UpgradeRolledBack, + ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, UPGRADE_CANCELLED_TOPIC, + UPGRADE_PREPARED_TOPIC, UPGRADE_ROLLED_BACK_TOPIC, +}; diff --git a/standards/dusk-contract-standards/src/proxy/state_store.rs b/standards/dusk-contract-standards/src/proxy/state_store.rs new file mode 100644 index 00000000..1485fa89 --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/state_store.rs @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Proxy-owned state store. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +/// Key-value state store for proxy-owned implementation state. +#[derive(Clone, Debug, Default)] +pub struct StateStore { + words: BTreeMap<[u8; 32], [u8; 32]>, + bytes: BTreeMap<[u8; 32], Vec>, + namespaced_words: BTreeMap<([u8; 32], [u8; 32]), [u8; 32]>, + namespaced_bytes: BTreeMap<([u8; 32], [u8; 32]), Vec>, +} + +impl StateStore { + /// Creates empty state. + pub const fn new() -> Self { + Self { + words: BTreeMap::new(), + bytes: BTreeMap::new(), + namespaced_words: BTreeMap::new(), + namespaced_bytes: BTreeMap::new(), + } + } + + /// Reads a word. + pub fn get_word(&self, key: [u8; 32]) -> [u8; 32] { + self.words.get(&key).copied().unwrap_or([0u8; 32]) + } + + /// Writes a word. + pub fn set_word(&mut self, key: [u8; 32], value: [u8; 32]) { + self.words.insert(key, value); + } + + /// Reads bytes. + pub fn get_bytes(&self, key: [u8; 32]) -> Vec { + self.bytes.get(&key).cloned().unwrap_or_default() + } + + /// Writes bytes. + pub fn set_bytes(&mut self, key: [u8; 32], value: Vec) { + self.bytes.insert(key, value); + } + + /// Reads a namespaced word. + pub fn get_namespaced_word( + &self, + namespace: [u8; 32], + key: [u8; 32], + ) -> [u8; 32] { + self.namespaced_words + .get(&(namespace, key)) + .copied() + .unwrap_or([0u8; 32]) + } + + /// Writes a namespaced word. + pub fn set_namespaced_word( + &mut self, + namespace: [u8; 32], + key: [u8; 32], + value: [u8; 32], + ) { + self.namespaced_words.insert((namespace, key), value); + } + + /// Reads namespaced bytes. + pub fn get_namespaced_bytes( + &self, + namespace: [u8; 32], + key: [u8; 32], + ) -> Vec { + self.namespaced_bytes + .get(&(namespace, key)) + .cloned() + .unwrap_or_default() + } + + /// Writes namespaced bytes. + pub fn set_namespaced_bytes( + &mut self, + namespace: [u8; 32], + key: [u8; 32], + value: Vec, + ) { + self.namespaced_bytes.insert((namespace, key), value); + } + + /// Deletes a key from both stores. + pub fn delete(&mut self, key: [u8; 32]) { + self.words.remove(&key); + self.bytes.remove(&key); + } + + /// Deletes a key from both namespaced stores. + pub fn delete_namespaced(&mut self, namespace: [u8; 32], key: [u8; 32]) { + self.namespaced_words.remove(&(namespace, key)); + self.namespaced_bytes.remove(&(namespace, key)); + } +} diff --git a/standards/dusk-contract-standards/src/proxy/upgrade.rs b/standards/dusk-contract-standards/src/proxy/upgrade.rs new file mode 100644 index 00000000..084405cb --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/upgrade.rs @@ -0,0 +1,367 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Upgrade admin state machine. + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// Upgrade prepared event topic. +pub const UPGRADE_PREPARED_TOPIC: &str = "proxy/upgrade_prepared"; +/// Upgrade activated event topic. +pub const UPGRADE_ACTIVATED_TOPIC: &str = "proxy/upgrade_activated"; +/// Pending upgrade cancelled event topic. +pub const UPGRADE_CANCELLED_TOPIC: &str = "proxy/upgrade_cancelled"; +/// Upgrade rolled back event topic. +pub const UPGRADE_ROLLED_BACK_TOPIC: &str = "proxy/upgrade_rolled_back"; +/// Rollback window finalized event topic. +pub const ROLLBACK_FINALIZED_TOPIC: &str = "proxy/rollback_finalized"; + +/// Pending upgrade data. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PendingUpgrade { + /// New implementation id. + pub implementation: ContractId, + /// Earliest activation timestamp/height unit selected by the contract. + pub eta: u64, + /// Migration payload. + pub migrate_data: Vec, +} + +/// Upgrade prepared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradePrepared { + /// New implementation id. + pub implementation: ContractId, + /// Earliest activation timestamp/height unit selected by the contract. + pub eta: u64, +} + +/// Upgrade activated event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeActivated { + /// Previous implementation id. + pub previous_implementation: ContractId, + /// Active implementation id. + pub implementation: ContractId, + /// Rollback deadline. + pub rollback_deadline: u64, +} + +/// Pending upgrade cancelled event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeCancelled { + /// Cancelled implementation id. + pub implementation: ContractId, +} + +/// Upgrade rolled back event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeRolledBack { + /// Implementation that was rolled back from. + pub from_implementation: ContractId, + /// Restored implementation id. + pub restored_implementation: ContractId, +} + +/// Rollback window finalized event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RollbackFinalized { + /// Active implementation id. + pub implementation: ContractId, +} + +impl dusk_forge::ContractEvent for UpgradePrepared { + const TOPICS: &'static [&'static str] = &[UPGRADE_PREPARED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeActivated { + const TOPICS: &'static [&'static str] = &[UPGRADE_ACTIVATED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeCancelled { + const TOPICS: &'static [&'static str] = &[UPGRADE_CANCELLED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeRolledBack { + const TOPICS: &'static [&'static str] = &[UPGRADE_ROLLED_BACK_TOPIC]; +} + +impl dusk_forge::ContractEvent for RollbackFinalized { + const TOPICS: &'static [&'static str] = &[ROLLBACK_FINALIZED_TOPIC]; +} + +/// Dusk-native upgrade controller. +#[derive(Clone, Debug)] +pub struct UpgradeAdmin { + admin: Principal, + implementation: ContractId, + previous_implementation: Option, + pending: Option, + delay: u64, + rollback_window: u64, + rollback_deadline: u64, +} + +impl UpgradeAdmin { + /// Creates an upgrade admin. + pub fn new( + admin: Principal, + implementation: ContractId, + delay: u64, + rollback_window: u64, + ) -> Self { + if admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if is_zero_contract_id(implementation) { + panic!("Proxy: invalid implementation"); + } + Self { + admin, + implementation, + previous_implementation: None, + pending: None, + delay, + rollback_window, + rollback_deadline: 0, + } + } + + /// Returns the admin. + pub const fn admin(&self) -> Principal { + self.admin + } + + /// Authorizes the upgrade admin through runtime context or signed + /// authorization. + pub fn authorize_admin( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_admin_with(&mut authorizer, authorization) + } + + /// Authorizes the upgrade admin with a reusable call authorizer. + pub fn authorize_admin_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + authorizer.require_principal_unbound(self.admin, authorization) + } + + /// Authorizes the upgrade admin through runtime context or an action-bound + /// signed authorization. + pub fn authorize_admin_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_admin_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes the upgrade admin with a reusable call authorizer and exact + /// call envelope for signed fallbacks. + pub fn authorize_admin_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + authorizer.require_principal_action(self.admin, authorization, envelope) + } + + /// Returns the active implementation. + pub const fn implementation(&self) -> ContractId { + self.implementation + } + + /// Returns pending upgrade. + pub const fn pending(&self) -> Option<&PendingUpgrade> { + self.pending.as_ref() + } + + /// Returns rollback deadline. + pub const fn rollback_deadline(&self) -> u64 { + self.rollback_deadline + } + + /// Prepares an upgrade. + pub fn prepare( + &mut self, + caller: Principal, + implementation: ContractId, + now: u64, + migrate_data: Vec, + ) -> UpgradePrepared { + self.assert_admin(caller); + if is_zero_contract_id(implementation) { + panic!("Proxy: invalid implementation"); + } + let eta = now.checked_add(self.delay).expect(error::OVERFLOW); + self.pending = Some(PendingUpgrade { + implementation, + eta, + migrate_data, + }); + UpgradePrepared { + implementation, + eta, + } + } + + /// Activates a prepared upgrade and returns migration data. + pub fn activate(&mut self, caller: Principal, now: u64) -> Vec { + self.activate_with_event(caller, now).0 + } + + /// Activates a prepared upgrade and returns migration data plus event data. + pub fn activate_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> (Vec, UpgradeActivated) { + self.assert_admin(caller); + let eta = self + .pending + .as_ref() + .unwrap_or_else(|| panic!("Proxy: no pending upgrade")) + .eta; + if now < eta { + panic!("Proxy: upgrade delay not elapsed"); + } + let rollback_deadline = now + .checked_add(self.rollback_window) + .expect(error::OVERFLOW); + let pending = self.pending.take().expect("pending was checked above"); + let previous = self.implementation; + self.previous_implementation = Some(previous); + self.implementation = pending.implementation; + self.rollback_deadline = rollback_deadline; + let event = UpgradeActivated { + previous_implementation: previous, + implementation: pending.implementation, + rollback_deadline: self.rollback_deadline, + }; + (pending.migrate_data, event) + } + + /// Cancels a pending upgrade. + pub fn cancel_pending(&mut self, caller: Principal) -> UpgradeCancelled { + self.assert_admin(caller); + let pending = self + .pending + .take() + .unwrap_or_else(|| panic!("Proxy: no pending upgrade")); + UpgradeCancelled { + implementation: pending.implementation, + } + } + + /// Rolls back to the previous implementation. + pub fn rollback(&mut self, caller: Principal, now: u64) { + let _ = self.rollback_with_event(caller, now); + } + + /// Rolls back to the previous implementation and returns event data. + pub fn rollback_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> UpgradeRolledBack { + self.assert_admin(caller); + if self.rollback_deadline == 0 || now > self.rollback_deadline { + panic!("Proxy: rollback window closed"); + } + let from_implementation = self.implementation; + let previous = self + .previous_implementation + .take() + .unwrap_or_else(|| panic!("Proxy: no previous implementation")); + self.implementation = previous; + self.pending = None; + self.rollback_deadline = 0; + UpgradeRolledBack { + from_implementation, + restored_implementation: previous, + } + } + + /// Finalizes rollback window after it has elapsed. + pub fn finalize_rollback_window(&mut self, caller: Principal, now: u64) { + let _ = self.finalize_rollback_window_with_event(caller, now); + } + + /// Finalizes rollback window after it has elapsed and returns event data. + pub fn finalize_rollback_window_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> RollbackFinalized { + self.assert_admin(caller); + if self.rollback_deadline != 0 && now < self.rollback_deadline { + panic!("Proxy: rollback window open"); + } + self.previous_implementation = None; + self.rollback_deadline = 0; + RollbackFinalized { + implementation: self.implementation, + } + } + + fn assert_admin(&self, caller: Principal) { + if caller != self.admin { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +fn is_zero_contract_id(id: ContractId) -> bool { + id.to_bytes().iter().all(|byte| *byte == 0) +} diff --git a/standards/dusk-contract-standards/src/security/mod.rs b/standards/dusk-contract-standards/src/security/mod.rs new file mode 100644 index 00000000..d7115562 --- /dev/null +++ b/standards/dusk-contract-standards/src/security/mod.rs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Security primitives. + +pub mod reentrancy_guard; + +pub use reentrancy_guard::{ReentrancyGuard, ReentrancyLock}; diff --git a/standards/dusk-contract-standards/src/security/reentrancy_guard.rs b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs new file mode 100644 index 00000000..2a860365 --- /dev/null +++ b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs @@ -0,0 +1,68 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Reentrancy guard. + +/// Reentrancy guard module. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReentrancyGuard { + entered: bool, +} + +/// Active reentrancy lock. +/// +/// Dropping the lock exits the guarded section. This keeps native tests and +/// non-aborting hosts from leaving the guard stuck when a guarded closure +/// panics. +#[derive(Debug)] +pub struct ReentrancyLock<'a> { + entered: &'a mut bool, +} + +impl ReentrancyGuard { + /// Creates a new guard. + pub const fn new() -> Self { + Self { entered: false } + } + + /// Returns whether the guard is currently entered. + pub const fn entered(&self) -> bool { + self.entered + } + + /// Enters the guarded section. + pub fn enter(&mut self) { + if self.entered { + panic!("ReentrancyGuard: reentrant call"); + } + self.entered = true; + } + + /// Exits the guarded section. + pub fn exit(&mut self) { + self.entered = false; + } + + /// Enters the guarded section and returns an RAII lock. + pub fn lock(&mut self) -> ReentrancyLock<'_> { + self.enter(); + ReentrancyLock { + entered: &mut self.entered, + } + } + + /// Runs a closure in a guarded section. + pub fn run(&mut self, f: impl FnOnce() -> R) -> R { + let _lock = self.lock(); + f() + } +} + +impl Drop for ReentrancyLock<'_> { + fn drop(&mut self) { + *self.entered = false; + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/events.rs b/standards/dusk-contract-standards/src/token/drc20/events.rs new file mode 100644 index 00000000..909852cf --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/events.rs @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC20 event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +/// Transfer event topic. +pub const TRANSFER_TOPIC: &str = "drc20/transfer"; +/// Approval event topic. +pub const APPROVAL_TOPIC: &str = "drc20/approval"; + +/// Transfer event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Transfer { + /// Sender. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Approval event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Approval { + /// Owner. + pub owner: Principal, + /// Spender. + pub spender: Principal, + /// Amount. + pub amount: u64, +} + +impl dusk_forge::ContractEvent for Transfer { + const TOPICS: &'static [&'static str] = &[TRANSFER_TOPIC]; +} + +impl dusk_forge::ContractEvent for Approval { + const TOPICS: &'static [&'static str] = &[APPROVAL_TOPIC]; +} diff --git a/standards/dusk-contract-standards/src/token/drc20/extensions.rs b/standards/dusk-contract-standards/src/token/drc20/extensions.rs new file mode 100644 index 00000000..078e4707 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/extensions.rs @@ -0,0 +1,272 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Optional DRC20 policy helpers. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; + +/// Supply cap policy. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SupplyCap { + cap: u64, +} + +impl SupplyCap { + /// Creates a supply cap. + pub const fn new(cap: u64) -> Self { + Self { cap } + } + + /// Returns the cap. + pub const fn cap(&self) -> u64 { + self.cap + } + + /// Sets a new cap. The new cap must not be below current supply. + pub fn set_cap(&mut self, current_supply: u64, cap: u64) { + if cap < current_supply { + panic!("DRC20: cap below current supply"); + } + self.cap = cap; + } + + /// Returns remaining mintable supply. + pub const fn remaining(&self, current_supply: u64) -> u64 { + self.cap.saturating_sub(current_supply) + } + + /// Panics unless minting `amount` keeps supply within cap. + pub fn assert_mint(&self, current_supply: u64, amount: u64) { + let next = current_supply.checked_add(amount).expect(error::OVERFLOW); + if next > self.cap { + panic!("DRC20: cap exceeded"); + } + } +} + +/// Vote or accounting checkpoint. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Checkpoint { + /// Block height, timestamp, epoch, or app-selected timepoint. + pub key: u64, + /// Value at that timepoint. + pub value: u64, +} + +/// Ordered checkpoint trace. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Checkpoints { + checkpoints: Vec, +} + +impl Checkpoints { + /// Creates an empty trace. + pub const fn new() -> Self { + Self { + checkpoints: Vec::new(), + } + } + + /// Returns all checkpoints. + pub fn entries(&self) -> &[Checkpoint] { + &self.checkpoints + } + + /// Returns latest value or zero. + pub fn latest(&self) -> u64 { + self.checkpoints + .last() + .map(|checkpoint| checkpoint.value) + .unwrap_or(0) + } + + /// Writes a checkpoint. + pub fn push(&mut self, key: u64, value: u64) { + if let Some(last) = self.checkpoints.last_mut() { + if key < last.key { + panic!("Checkpoints: non-monotonic key"); + } + if key == last.key { + last.value = value; + return; + } + } + self.checkpoints.push(Checkpoint { key, value }); + } + + fn assert_can_push(&self, key: u64) { + if let Some(last) = self.checkpoints.last() { + if key < last.key { + panic!("Checkpoints: non-monotonic key"); + } + } + } + + /// Returns value at or before `key`. + pub fn get_at(&self, key: u64) -> u64 { + let mut low = 0usize; + let mut high = self.checkpoints.len(); + while low < high { + let mid = (low + high) / 2; + if self.checkpoints[mid].key <= key { + low = mid + 1; + } else { + high = mid; + } + } + if low == 0 { + 0 + } else { + self.checkpoints[low - 1].value + } + } +} + +/// DRC20 voting-unit checkpoint store. +/// +/// Composing contracts call `move_units` after mint, burn, and transfer hooks +/// when they want historical vote/accounting queries. +#[derive(Clone, Debug, Default)] +pub struct VotingUnits { + accounts: BTreeMap, + total_supply: Checkpoints, +} + +impl VotingUnits { + /// Creates empty voting-unit state. + pub const fn new() -> Self { + Self { + accounts: BTreeMap::new(), + total_supply: Checkpoints::new(), + } + } + + /// Returns latest account units. + pub fn latest_votes(&self, account: Principal) -> u64 { + self.accounts + .get(&account) + .map(Checkpoints::latest) + .unwrap_or(0) + } + + /// Returns historical account units. + pub fn past_votes(&self, account: Principal, timepoint: u64) -> u64 { + self.accounts + .get(&account) + .map(|trace| trace.get_at(timepoint)) + .unwrap_or(0) + } + + /// Returns latest total units. + pub fn latest_total_supply(&self) -> u64 { + self.total_supply.latest() + } + + /// Returns historical total units. + pub fn past_total_supply(&self, timepoint: u64) -> u64 { + self.total_supply.get_at(timepoint) + } + + /// Writes an account checkpoint directly. + pub fn write_votes( + &mut self, + account: Principal, + timepoint: u64, + value: u64, + ) { + self.accounts + .entry(account) + .or_default() + .push(timepoint, value); + } + + /// Moves units between accounts, minting from `None` or burning to `None`. + pub fn move_units( + &mut self, + from: Option, + to: Option, + amount: u64, + timepoint: u64, + ) { + if amount == 0 || from == to { + return; + } + + let from_next = if let Some(from) = from { + let current = self.latest_votes(from); + if current < amount { + panic!("VotingUnits: insufficient units"); + } + Some((from, current - amount)) + } else { + None + }; + + let to_next = if let Some(to) = to { + let current = self.latest_votes(to); + let next = current.checked_add(amount).expect(error::OVERFLOW); + Some((to, next)) + } else { + None + }; + + let total_next = match (from, to) { + (None, Some(_)) => Some( + self.latest_total_supply() + .checked_add(amount) + .expect(error::OVERFLOW), + ), + (Some(_), None) => { + let current = self.latest_total_supply(); + if current < amount { + panic!("VotingUnits: total supply underflow"); + } + Some(current - amount) + } + _ => None, + }; + + if let Some((from, _)) = from_next { + self.assert_can_write_votes(from, timepoint); + } + if let Some((to, _)) = to_next { + self.assert_can_write_votes(to, timepoint); + } + if total_next.is_some() { + self.total_supply.assert_can_push(timepoint); + } + + if let Some((from, value)) = from_next { + self.write_votes(from, timepoint, value); + } + if let Some((to, value)) = to_next { + self.write_votes(to, timepoint, value); + } + if let Some(value) = total_next { + self.total_supply.push(timepoint, value); + } + } + + fn assert_can_write_votes(&self, account: Principal, timepoint: u64) { + if let Some(trace) = self.accounts.get(&account) { + trace.assert_can_push(timepoint); + } + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/mod.rs b/standards/dusk-contract-standards/src/token/drc20/mod.rs new file mode 100644 index 00000000..bd633e28 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/mod.rs @@ -0,0 +1,350 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC20 fungible token primitive. + +pub mod events; +pub mod extensions; +pub mod types; + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::core::{error, Principal}; +use events::{Approval, Transfer}; +pub use extensions::{Checkpoint, Checkpoints, SupplyCap, VotingUnits}; +pub use types::{ + Allowance, ApproveCall, BalanceOf, DecreaseAllowanceCall, + IncreaseAllowanceCall, Init, InitBalance, SignedApproveCall, TransferCall, + TransferFromCall, +}; + +/// Reserved zero principal. +pub const ZERO_PRINCIPAL: Principal = + Principal::Contract(dusk_core::abi::ContractId::from_bytes([0u8; 32])); + +/// DRC20 token state and logic. +#[derive(Clone, Debug)] +pub struct Drc20 { + initialized: bool, + name: String, + symbol: String, + decimals: u8, + balances: BTreeMap, + allowances: BTreeMap<(Principal, Principal), u64>, + supply: u64, +} + +impl Drc20 { + /// Creates empty token state. + pub const fn new() -> Self { + Self { + initialized: false, + name: String::new(), + symbol: String::new(), + decimals: 0, + balances: BTreeMap::new(), + allowances: BTreeMap::new(), + supply: 0, + } + } + + /// Initializes token metadata and initial supply. + pub fn init(&mut self, args: Init) -> Vec { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + let mut initial_supply = 0u64; + for entry in &args.initial_balances { + if entry.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + initial_supply = initial_supply + .checked_add(entry.amount) + .expect(error::OVERFLOW); + } + + self.initialized = true; + self.name = args.name; + self.symbol = args.symbol; + self.decimals = args.decimals; + + let mut events = Vec::new(); + for entry in args.initial_balances { + if entry.amount == 0 { + continue; + } + self.mint_internal(entry.account, entry.amount); + events.push(Transfer { + from: ZERO_PRINCIPAL, + to: entry.account, + amount: entry.amount, + }); + } + events + } + + /// Token name. + pub fn name(&self) -> String { + self.assert_initialized(); + self.name.clone() + } + + /// Token symbol. + pub fn symbol(&self) -> String { + self.assert_initialized(); + self.symbol.clone() + } + + /// Token decimals. + pub fn decimals(&self) -> u8 { + self.assert_initialized(); + self.decimals + } + + /// Total supply. + pub fn total_supply(&self) -> u64 { + self.assert_initialized(); + self.supply + } + + /// Balance of `account`. + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.assert_initialized(); + self.balances.get(&args.account).copied().unwrap_or(0) + } + + /// Allowance from owner to spender. + pub fn allowance(&self, args: Allowance) -> u64 { + self.assert_initialized(); + self.allowances + .get(&(args.owner, args.spender)) + .copied() + .unwrap_or(0) + } + + /// Transfers from caller to recipient. + pub fn transfer( + &mut self, + caller: Principal, + args: TransferCall, + ) -> Transfer { + self.assert_initialized(); + self.transfer_internal(caller, args.to, args.amount) + } + + /// Approves spender. + pub fn approve( + &mut self, + caller: Principal, + args: ApproveCall, + ) -> Approval { + self.approve_for(caller, args) + } + + /// Approves spender for an explicitly authorized owner. + pub fn approve_for( + &mut self, + owner: Principal, + args: ApproveCall, + ) -> Approval { + self.assert_initialized(); + if owner.is_zero() || args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.allowances.insert((owner, args.spender), args.amount); + Approval { + owner, + spender: args.spender, + amount: args.amount, + } + } + + /// Increases spender allowance. + pub fn increase_allowance( + &mut self, + caller: Principal, + args: IncreaseAllowanceCall, + ) -> Approval { + self.assert_initialized(); + if caller.is_zero() || args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.allowance(Allowance { + owner: caller, + spender: args.spender, + }); + let amount = current + .checked_add(args.added_amount) + .expect(error::OVERFLOW); + self.allowances.insert((caller, args.spender), amount); + Approval { + owner: caller, + spender: args.spender, + amount, + } + } + + /// Decreases spender allowance. + pub fn decrease_allowance( + &mut self, + caller: Principal, + args: DecreaseAllowanceCall, + ) -> Approval { + self.assert_initialized(); + if caller.is_zero() || args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.allowance(Allowance { + owner: caller, + spender: args.spender, + }); + if current < args.subtracted_amount { + panic!("DRC20: allowance below zero"); + } + let amount = current - args.subtracted_amount; + self.allowances.insert((caller, args.spender), amount); + Approval { + owner: caller, + spender: args.spender, + amount, + } + } + + /// Transfers using allowance. + pub fn transfer_from( + &mut self, + caller: Principal, + args: TransferFromCall, + ) -> Transfer { + self.assert_initialized(); + if caller.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.assert_can_transfer(args.owner, args.to, args.amount); + let current = self.allowance(Allowance { + owner: args.owner, + spender: caller, + }); + if current < args.amount { + panic!("DRC20: allowance too low"); + } + self.allowances + .insert((args.owner, caller), current - args.amount); + self.transfer_internal(args.owner, args.to, args.amount) + } + + /// Mints tokens. Access control belongs in the composing contract. + pub fn mint(&mut self, to: Principal, amount: u64) -> Transfer { + self.assert_initialized(); + self.mint_internal(to, amount); + Transfer { + from: ZERO_PRINCIPAL, + to, + amount, + } + } + + /// Burns caller tokens. + pub fn burn(&mut self, from: Principal, amount: u64) -> Transfer { + self.assert_initialized(); + self.burn_internal(from, amount); + Transfer { + from, + to: ZERO_PRINCIPAL, + amount, + } + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn mint_internal(&mut self, to: Principal, amount: u64) { + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.balances.get(&to).copied().unwrap_or(0); + let next = current.checked_add(amount).expect(error::OVERFLOW); + let supply = self.supply.checked_add(amount).expect(error::OVERFLOW); + if next == 0 { + self.balances.remove(&to); + } else { + self.balances.insert(to, next); + } + self.supply = supply; + } + + fn burn_internal(&mut self, from: Principal, amount: u64) { + if from.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.balances.get(&from).copied().unwrap_or(0); + if current < amount { + panic!("DRC20: balance too low"); + } + let supply = self.supply.checked_sub(amount).expect(error::UNDERFLOW); + if amount != 0 { + let next = current - amount; + if next == 0 { + self.balances.remove(&from); + } else { + self.balances.insert(from, next); + } + self.supply = supply; + } + } + + fn transfer_internal( + &mut self, + from: Principal, + to: Principal, + amount: u64, + ) -> Transfer { + self.assert_can_transfer(from, to, amount); + if amount != 0 { + if from == to { + return Transfer { from, to, amount }; + } + let current = self.balances.get(&from).copied().unwrap_or(0); + let next = current - amount; + if next == 0 { + self.balances.remove(&from); + } else { + self.balances.insert(from, next); + } + let to_balance = self.balances.get(&to).copied().unwrap_or(0); + self.balances.insert(to, to_balance + amount); + } + Transfer { from, to, amount } + } + + fn assert_can_transfer(&self, from: Principal, to: Principal, amount: u64) { + if from.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.balances.get(&from).copied().unwrap_or(0); + if current < amount { + panic!("DRC20: balance too low"); + } + if from != to { + let to_balance = self.balances.get(&to).copied().unwrap_or(0); + to_balance.checked_add(amount).expect(error::OVERFLOW); + } + } +} + +impl Default for Drc20 { + fn default() -> Self { + Self::new() + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/types.rs b/standards/dusk-contract-standards/src/token/drc20/types.rs new file mode 100644 index 00000000..4b54c80c --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/types.rs @@ -0,0 +1,150 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC20 rkyv-compatible call types. + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::SignedAuthorization; +use crate::core::Principal; + +/// One initial balance entry. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct InitBalance { + /// Recipient. + pub account: Principal, + /// Amount. + pub amount: u64, +} + +/// Initialization input. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + /// Name. + pub name: String, + /// Symbol. + pub symbol: String, + /// Decimals. + pub decimals: u8, + /// Initial balances. + pub initial_balances: Vec, +} + +/// Balance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceOf { + /// Account. + pub account: Principal, +} + +/// Allowance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Allowance { + /// Owner. + pub owner: Principal, + /// Spender. + pub spender: Principal, +} + +/// Transfer call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferCall { + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Approve call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApproveCall { + /// Spender. + pub spender: Principal, + /// Amount. + pub amount: u64, +} + +/// Increase allowance call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct IncreaseAllowanceCall { + /// Spender. + pub spender: Principal, + /// Added amount. + pub added_amount: u64, +} + +/// Decrease allowance call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DecreaseAllowanceCall { + /// Spender. + pub spender: Principal, + /// Subtracted amount. + pub subtracted_amount: u64, +} + +/// Transfer from call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferFromCall { + /// Owner. + pub owner: Principal, + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Signed approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedApproveCall { + /// Allowance owner. Must match the signed action principal. + pub owner: Principal, + /// Spender. + pub spender: Principal, + /// Allowance amount. + pub amount: u64, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} diff --git a/standards/dusk-contract-standards/src/token/drc721/events.rs b/standards/dusk-contract-standards/src/token/drc721/events.rs new file mode 100644 index 00000000..001d98d1 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/events.rs @@ -0,0 +1,157 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC721 event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +/// Transfer event topic. +pub const TRANSFER_TOPIC: &str = "drc721/transfer"; +/// Approval event topic. +pub const APPROVAL_TOPIC: &str = "drc721/approval"; +/// Approval-for-all event topic. +pub const APPROVAL_FOR_ALL_TOPIC: &str = "drc721/approval_for_all"; +/// Default royalty set event topic. +pub const DEFAULT_ROYALTY_SET_TOPIC: &str = "drc721/default_royalty_set"; +/// Default royalty cleared event topic. +pub const DEFAULT_ROYALTY_CLEARED_TOPIC: &str = + "drc721/default_royalty_cleared"; +/// Token royalty set event topic. +pub const TOKEN_ROYALTY_SET_TOPIC: &str = "drc721/token_royalty_set"; +/// Token royalty cleared event topic. +pub const TOKEN_ROYALTY_CLEARED_TOPIC: &str = "drc721/token_royalty_cleared"; + +/// Transfer event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Transfer { + /// Sender. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Token id. + pub token_id: u64, +} + +/// Approval event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Approval { + /// Owner. + pub owner: Principal, + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, +} + +/// Approval-for-all event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApprovalForAll { + /// Owner. + pub owner: Principal, + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, +} + +/// Default royalty set event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DefaultRoyaltySet { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Royalty receiver. + pub receiver: Principal, + /// Royalty basis points. + pub basis_points: u16, +} + +/// Default royalty cleared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DefaultRoyaltyCleared { + /// Principal that authorized the royalty change. + pub operator: Principal, +} + +/// Token royalty set event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenRoyaltySet { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Token id. + pub token_id: u64, + /// Royalty receiver. + pub receiver: Principal, + /// Royalty basis points. + pub basis_points: u16, +} + +/// Token royalty cleared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenRoyaltyCleared { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Token id. + pub token_id: u64, +} + +impl dusk_forge::ContractEvent for Transfer { + const TOPICS: &'static [&'static str] = &[TRANSFER_TOPIC]; +} + +impl dusk_forge::ContractEvent for Approval { + const TOPICS: &'static [&'static str] = &[APPROVAL_TOPIC]; +} + +impl dusk_forge::ContractEvent for ApprovalForAll { + const TOPICS: &'static [&'static str] = &[APPROVAL_FOR_ALL_TOPIC]; +} + +impl dusk_forge::ContractEvent for DefaultRoyaltySet { + const TOPICS: &'static [&'static str] = &[DEFAULT_ROYALTY_SET_TOPIC]; +} + +impl dusk_forge::ContractEvent for DefaultRoyaltyCleared { + const TOPICS: &'static [&'static str] = &[DEFAULT_ROYALTY_CLEARED_TOPIC]; +} + +impl dusk_forge::ContractEvent for TokenRoyaltySet { + const TOPICS: &'static [&'static str] = &[TOKEN_ROYALTY_SET_TOPIC]; +} + +impl dusk_forge::ContractEvent for TokenRoyaltyCleared { + const TOPICS: &'static [&'static str] = &[TOKEN_ROYALTY_CLEARED_TOPIC]; +} diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs new file mode 100644 index 00000000..c43d8938 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -0,0 +1,379 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC721 non-fungible token primitive. + +pub mod events; +pub mod royalty; +pub mod types; + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString}; + +use crate::core::{error, Principal}; +use events::{Approval, ApprovalForAll, Transfer}; +pub use royalty::{ + RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, MAX_BASIS_POINTS, +}; +pub use types::{ + ApproveCall, BalanceOf, GetApproved, Init, InitToken, IsApprovedForAll, + OwnerOf, SetApprovalForAllCall, SignedApproveCall, + SignedSetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, TokenUri, + TokensOf, TransferFromCall, +}; + +/// Reserved zero principal. +pub const ZERO_PRINCIPAL: Principal = + Principal::Contract(dusk_core::abi::ContractId::from_bytes([0u8; 32])); + +/// DRC721 token state and logic. +#[derive(Clone, Debug)] +pub struct Drc721 { + initialized: bool, + name: String, + symbol: String, + owners: BTreeMap, + balances: BTreeMap, + token_approvals: BTreeMap, + operator_approvals: BTreeMap<(Principal, Principal), bool>, + supply: u64, + base_uri: String, +} + +impl Drc721 { + /// Creates empty state. + pub const fn new() -> Self { + Self { + initialized: false, + name: String::new(), + symbol: String::new(), + owners: BTreeMap::new(), + balances: BTreeMap::new(), + token_approvals: BTreeMap::new(), + operator_approvals: BTreeMap::new(), + supply: 0, + base_uri: String::new(), + } + } + + /// Initializes token metadata and initial distribution. + pub fn init(&mut self, args: Init) -> alloc::vec::Vec { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + let mut token_ids = BTreeSet::new(); + for token in &args.initial_tokens { + if token.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !token_ids.insert(token.token_id) { + panic!("DRC721: token already minted"); + } + } + + self.initialized = true; + self.name = args.name; + self.symbol = args.symbol; + self.base_uri = args.base_uri; + let mut out = alloc::vec::Vec::new(); + for token in args.initial_tokens { + self.mint_internal(token.account, token.token_id); + out.push(Transfer { + from: ZERO_PRINCIPAL, + to: token.account, + token_id: token.token_id, + }); + } + out + } + + /// Name. + pub fn name(&self) -> String { + self.assert_initialized(); + self.name.clone() + } + + /// Symbol. + pub fn symbol(&self) -> String { + self.assert_initialized(); + self.symbol.clone() + } + + /// Base URI. + pub fn base_uri(&self) -> String { + self.assert_initialized(); + self.base_uri.clone() + } + + /// Token URI. + pub fn token_uri(&self, args: TokenUri) -> String { + self.assert_initialized(); + self.require_exists(args.token_id); + if self.base_uri.is_empty() { + return String::new(); + } + let mut uri = self.base_uri.clone(); + uri.push_str(&args.token_id.to_string()); + uri + } + + /// Token id by global index. + pub fn token_by_index(&self, args: TokenByIndex) -> u64 { + self.assert_initialized(); + self.owners + .keys() + .nth(args.index as usize) + .copied() + .unwrap_or_else(|| panic!("DRC721: global index out of bounds")) + } + + /// Token id by owner-local index. + pub fn token_of_owner_by_index(&self, args: TokenOfOwnerByIndex) -> u64 { + self.assert_initialized(); + self.owners + .iter() + .filter_map(|(token_id, owner)| { + if *owner == args.owner { + Some(*token_id) + } else { + None + } + }) + .nth(args.index as usize) + .unwrap_or_else(|| panic!("DRC721: owner index out of bounds")) + } + + /// All token ids owned by an account. + pub fn tokens_of(&self, args: TokensOf) -> alloc::vec::Vec { + self.assert_initialized(); + self.owners + .iter() + .filter_map(|(token_id, owner)| { + if *owner == args.owner { + Some(*token_id) + } else { + None + } + }) + .collect() + } + + /// Total supply. + pub fn total_supply(&self) -> u64 { + self.assert_initialized(); + self.supply + } + + /// Balance of an account. + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.assert_initialized(); + if args.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.balances.get(&args.account).copied().unwrap_or(0) + } + + /// Owner of a token. + pub fn owner_of(&self, args: OwnerOf) -> Principal { + self.assert_initialized(); + self.owners + .get(&args.token_id) + .copied() + .unwrap_or_else(|| panic!("DRC721: token does not exist")) + } + + /// Approved principal for a token. + pub fn get_approved(&self, args: GetApproved) -> Principal { + self.assert_initialized(); + self.require_exists(args.token_id); + self.token_approvals + .get(&args.token_id) + .copied() + .unwrap_or(ZERO_PRINCIPAL) + } + + /// Operator approval. + pub fn is_approved_for_all(&self, args: IsApprovedForAll) -> bool { + self.assert_initialized(); + self.operator_approvals + .get(&(args.owner, args.operator)) + .copied() + .unwrap_or(false) + } + + /// Approves an account for a token. + pub fn approve( + &mut self, + caller: Principal, + args: ApproveCall, + ) -> Approval { + self.assert_initialized(); + let owner = self.owner_of(OwnerOf { + token_id: args.token_id, + }); + if args.approved == owner { + panic!("DRC721: approval to current owner"); + } + if caller != owner + && !self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }) + { + panic!("{}", error::UNAUTHORIZED); + } + if args.approved.is_zero() { + self.token_approvals.remove(&args.token_id); + } else { + self.token_approvals.insert(args.token_id, args.approved); + } + Approval { + owner, + approved: args.approved, + token_id: args.token_id, + } + } + + /// Sets operator approval. + pub fn set_approval_for_all( + &mut self, + caller: Principal, + args: SetApprovalForAllCall, + ) -> ApprovalForAll { + self.assert_initialized(); + if caller.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if args.operator.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if args.operator == caller { + panic!("DRC721: approval to current owner"); + } + self.operator_approvals + .insert((caller, args.operator), args.approved); + ApprovalForAll { + owner: caller, + operator: args.operator, + approved: args.approved, + } + } + + /// Transfers a token. + pub fn transfer_from( + &mut self, + caller: Principal, + args: TransferFromCall, + ) -> Transfer { + self.assert_initialized(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let owner = self.owner_of(OwnerOf { + token_id: args.token_id, + }); + if owner != args.from { + panic!("DRC721: incorrect owner"); + } + let approved = self.token_approvals.get(&args.token_id).copied(); + let operator = self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }); + if caller != owner && approved != Some(caller) && !operator { + panic!("{}", error::UNAUTHORIZED); + } + self.token_approvals.remove(&args.token_id); + self.owners.insert(args.token_id, args.to); + self.decrement_balance(owner); + self.increment_balance(args.to); + Transfer { + from: owner, + to: args.to, + token_id: args.token_id, + } + } + + /// Mints a token. Access control belongs in the composing contract. + pub fn mint(&mut self, to: Principal, token_id: u64) -> Transfer { + self.assert_initialized(); + self.mint_internal(to, token_id); + Transfer { + from: ZERO_PRINCIPAL, + to, + token_id, + } + } + + /// Burns a token. + pub fn burn(&mut self, caller: Principal, token_id: u64) -> Transfer { + self.assert_initialized(); + let owner = self.owner_of(OwnerOf { token_id }); + let approved = self.token_approvals.get(&token_id).copied(); + let operator = self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }); + if caller != owner && approved != Some(caller) && !operator { + panic!("{}", error::UNAUTHORIZED); + } + self.owners.remove(&token_id); + self.token_approvals.remove(&token_id); + self.decrement_balance(owner); + self.supply = self.supply.checked_sub(1).expect(error::UNDERFLOW); + Transfer { + from: owner, + to: ZERO_PRINCIPAL, + token_id, + } + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn mint_internal(&mut self, to: Principal, token_id: u64) { + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if self.owners.contains_key(&token_id) { + panic!("DRC721: token already minted"); + } + self.owners.insert(token_id, to); + self.increment_balance(to); + self.supply = self.supply.checked_add(1).expect(error::OVERFLOW); + } + + fn require_exists(&self, token_id: u64) { + if !self.owners.contains_key(&token_id) { + panic!("DRC721: token does not exist"); + } + } + + fn increment_balance(&mut self, owner: Principal) { + let bal = self.balances.entry(owner).or_insert(0); + *bal = bal.checked_add(1).expect(error::OVERFLOW); + } + + fn decrement_balance(&mut self, owner: Principal) { + let bal = self + .balances + .get_mut(&owner) + .unwrap_or_else(|| panic!("DRC721: incorrect owner")); + *bal = bal.checked_sub(1).expect(error::UNDERFLOW); + if *bal == 0 { + self.balances.remove(&owner); + } + } +} + +impl Default for Drc721 { + fn default() -> Self { + Self::new() + } +} diff --git a/standards/dusk-contract-standards/src/token/drc721/royalty.rs b/standards/dusk-contract-standards/src/token/drc721/royalty.rs new file mode 100644 index 00000000..d5124feb --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/royalty.rs @@ -0,0 +1,136 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Optional DRC721 royalty policy helper. + +use alloc::collections::BTreeMap; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; +use crate::token::drc721::ZERO_PRINCIPAL; + +/// Maximum royalty basis points. +pub const MAX_BASIS_POINTS: u16 = 10_000; + +/// Royalty policy for a token or collection. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyInfo { + /// Receiver principal. + pub receiver: Principal, + /// Royalty fraction in basis points. + pub basis_points: u16, +} + +/// Royalty quote for a sale price. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyQuote { + /// Receiver principal, or zero principal when no royalty exists. + pub receiver: Principal, + /// Royalty amount. + pub amount: u64, +} + +/// Royalty quote query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyQuery { + /// Token id. + pub token_id: u64, + /// Sale price used for royalty calculation. + pub sale_price: u64, +} + +/// Default plus token-specific royalty registry. +#[derive(Clone, Debug, Default)] +pub struct RoyaltyRegistry { + default: Option, + token_overrides: BTreeMap, +} + +impl RoyaltyRegistry { + /// Creates an empty registry. + pub const fn new() -> Self { + Self { + default: None, + token_overrides: BTreeMap::new(), + } + } + + /// Returns default royalty policy. + pub const fn default_royalty(&self) -> Option { + self.default + } + + /// Returns token royalty policy, falling back to default. + pub fn royalty_for(&self, token_id: u64) -> Option { + self.token_overrides + .get(&token_id) + .copied() + .or(self.default) + } + + /// Sets default royalty. + pub fn set_default_royalty(&mut self, info: RoyaltyInfo) { + validate(info); + self.default = Some(info); + } + + /// Clears default royalty. + pub fn clear_default_royalty(&mut self) { + self.default = None; + } + + /// Sets token-specific royalty. + pub fn set_token_royalty(&mut self, token_id: u64, info: RoyaltyInfo) { + validate(info); + self.token_overrides.insert(token_id, info); + } + + /// Clears token-specific royalty. + pub fn clear_token_royalty(&mut self, token_id: u64) { + self.token_overrides.remove(&token_id); + } + + /// Returns a royalty quote for `sale_price`. + pub fn royalty_info(&self, token_id: u64, sale_price: u64) -> RoyaltyQuote { + let Some(info) = self.royalty_for(token_id) else { + return RoyaltyQuote { + receiver: ZERO_PRINCIPAL, + amount: 0, + }; + }; + let amount = sale_price + .checked_mul(u64::from(info.basis_points)) + .expect(error::OVERFLOW) + / u64::from(MAX_BASIS_POINTS); + RoyaltyQuote { + receiver: info.receiver, + amount, + } + } +} + +fn validate(info: RoyaltyInfo) { + if info.receiver.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if info.basis_points > MAX_BASIS_POINTS { + panic!("DRC721: royalty exceeds sale price"); + } +} diff --git a/standards/dusk-contract-standards/src/token/drc721/types.rs b/standards/dusk-contract-standards/src/token/drc721/types.rs new file mode 100644 index 00000000..f312b6b3 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/types.rs @@ -0,0 +1,207 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! DRC721 rkyv-compatible call types. + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::SignedAuthorization; +use crate::core::Principal; + +/// Initial token. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct InitToken { + /// Owner. + pub account: Principal, + /// Token id. + pub token_id: u64, +} + +/// Initialization input. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + /// Name. + pub name: String, + /// Symbol. + pub symbol: String, + /// Base URI. + pub base_uri: String, + /// Initial tokens. + pub initial_tokens: Vec, +} + +/// Balance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceOf { + /// Account. + pub account: Principal, +} + +/// Owner query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct OwnerOf { + /// Token id. + pub token_id: u64, +} + +/// URI query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenUri { + /// Token id. + pub token_id: u64, +} + +/// Token by global index query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenByIndex { + /// Zero-based index. + pub index: u64, +} + +/// Token by owner index query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenOfOwnerByIndex { + /// Owner. + pub owner: Principal, + /// Zero-based owner-local index. + pub index: u64, +} + +/// Tokens owned by an account query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokensOf { + /// Owner. + pub owner: Principal, +} + +/// Approved query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct GetApproved { + /// Token id. + pub token_id: u64, +} + +/// Operator approval query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct IsApprovedForAll { + /// Owner. + pub owner: Principal, + /// Operator. + pub operator: Principal, +} + +/// Approve call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApproveCall { + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, +} + +/// Operator approval call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetApprovalForAllCall { + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, +} + +/// Signed token approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedApproveCall { + /// Token owner. Must match the signed action principal and current owner. + pub owner: Principal, + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} + +/// Signed operator approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedSetApprovalForAllCall { + /// Token owner. Must match the signed action principal. + pub owner: Principal, + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} + +/// Transfer call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferFromCall { + /// Current owner. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Token id. + pub token_id: u64, +} diff --git a/standards/dusk-contract-standards/src/token/mod.rs b/standards/dusk-contract-standards/src/token/mod.rs new file mode 100644 index 00000000..5c6f5418 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/mod.rs @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Dusk token primitives. + +pub mod drc20; +pub mod drc721; diff --git a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs new file mode 100644 index 00000000..0e25b7be --- /dev/null +++ b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs @@ -0,0 +1,1098 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; + +use dusk_contract_standards::access::events as access_events; +use dusk_contract_standards::core::Principal; +use dusk_contract_standards::governance::{ + MultisigAuthorityUpdated, MultisigOperationCancelled, + MultisigOperationConfirmed, MultisigOperationExecuted, + MultisigOperationProposed, MultisigTarget, MultisigTimeLimitsUpdated, + MULTISIG_AUTHORITY_UPDATED_TOPIC, MULTISIG_OPERATION_CANCELLED_TOPIC, + MULTISIG_OPERATION_CONFIRMED_TOPIC, MULTISIG_OPERATION_EXECUTED_TOPIC, + MULTISIG_OPERATION_PROPOSED_TOPIC, MULTISIG_TIME_LIMITS_UPDATED_TOPIC, +}; +use dusk_contract_standards::proxy::{ + RollbackFinalized, UpgradeActivated, UpgradeCancelled, UpgradePrepared, + UpgradeRolledBack, ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, + UPGRADE_CANCELLED_TOPIC, UPGRADE_PREPARED_TOPIC, UPGRADE_ROLLED_BACK_TOPIC, +}; +use dusk_contract_standards::token::drc20::events as drc20_events; +use dusk_contract_standards::token::drc721::{ + events as drc721_events, RoyaltyQuote, +}; +use dusk_core::abi::ContractId; +use dusk_core::transfer::data::ContractCall; +use dusk_data_driver::reader::DriverReader; +use proptest::prelude::*; +use proptest::test_runner::{Config as ProptestConfig, TestRunner}; +use rkyv::ser::serializers::AllocSerializer; +use serde_json::{json, Value}; + +const DRIVER_NAMES: [&str; 5] = [ + "authorization_counter", + "drc20_roles_pausable", + "drc721_collection", + "multisig_controller", + "proxy_counter", +]; + +#[derive(Clone, Copy, Debug)] +enum ReferenceContract { + AuthorizationCounter, + Drc20, + Drc721, + MultisigController, + ProxyCounter, +} + +#[derive(Clone, Debug)] +struct DriverCase { + contract: ReferenceContract, + selector: u8, + a: u8, + b: u8, + amount: u64, + flag: bool, +} + +struct CallSpec { + driver: &'static str, + function: &'static str, + input: Value, +} + +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_schemas_are_loadable() { + for name in DRIVER_NAMES { + let driver = load_driver(name); + let version = driver.get_version().expect("driver version"); + assert!(!version.is_empty(), "{name} returned an empty version"); + + let schema = driver.get_schema().expect("driver schema"); + let functions = schema + .get("functions") + .and_then(Value::as_array) + .unwrap_or_else(|| panic!("{name} schema has no functions array")); + assert!(!functions.is_empty(), "{name} schema has no functions"); + } +} + +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_inputs_roundtrip_and_reject_bad_shapes() { + let drivers = load_drivers(); + assert_rejects_bad_inputs(&drivers); + + let mut runner = TestRunner::new(ProptestConfig { + cases: env_usize("STANDARDS_DATA_DRIVER_FUZZ_CASES") + .or_else(|| env_usize("PROPTEST_CASES")) + .unwrap_or(512) as u32, + max_shrink_iters: env_usize("STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS") + .or_else(|| env_usize("PROPTEST_MAX_SHRINK_ITERS")) + .unwrap_or(2048) as u32, + ..ProptestConfig::default() + }); + + runner + .run(&driver_case_strategy(), |case| { + let call = call_for(&case); + let driver = drivers.get(call.driver).expect("loaded driver"); + assert_input_roundtrip(driver, &call)?; + assert_mutated_inputs_are_handled(driver, &call)?; + Ok(()) + }) + .expect("data-driver fuzz cases failed"); +} + +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_outputs_and_events_decode() { + let drivers = load_drivers(); + + assert_output_decodes( + drivers.get("authorization_counter").unwrap(), + "value", + &42u64, + ); + assert_output_decodes( + drivers.get("authorization_counter").unwrap(), + "last_authorizer", + &Some(principal_value(1)), + ); + + let drc20 = drivers.get("drc20_roles_pausable").unwrap(); + assert_output_decodes(drc20, "name", &"Audit Token".to_string()); + assert_output_decodes(drc20, "decimals", &9u8); + assert_output_decodes(drc20, "total_supply", &1_000u64); + assert_output_decodes(drc20, "has_role", &true); + assert_output_decodes(drc20, "paused", &false); + assert_event_decodes( + drc20, + drc20_events::TRANSFER_TOPIC, + &drc20_events::Transfer { + from: principal_value(1), + to: principal_value(2), + amount: 7, + }, + ); + assert_event_decodes( + drc20, + drc20_events::APPROVAL_TOPIC, + &drc20_events::Approval { + owner: principal_value(1), + spender: principal_value(2), + amount: 7, + }, + ); + assert_event_decodes( + drc20, + access_events::PAUSED_TOPIC, + &access_events::Paused { + account: principal_value(1), + }, + ); + assert_event_decodes( + drc20, + access_events::ROLE_GRANTED_TOPIC, + &access_events::RoleGranted { + role: [3u8; 32], + account: principal_value(1), + sender: principal_value(2), + }, + ); + + let drc721 = drivers.get("drc721_collection").unwrap(); + assert_output_decodes(drc721, "base_uri", &"ipfs://audit/".to_string()); + assert_output_decodes(drc721, "tokens_of", &vec![1u64, 2, 3]); + assert_output_decodes(drc721, "owner_of", &principal_value(1)); + assert_output_decodes( + drc721, + "royalty_info", + &RoyaltyQuote { + receiver: principal_value(2), + amount: 50, + }, + ); + assert_event_decodes( + drc721, + drc721_events::TRANSFER_TOPIC, + &drc721_events::Transfer { + from: principal_value(1), + to: principal_value(2), + token_id: 9, + }, + ); + assert_event_decodes( + drc721, + drc721_events::APPROVAL_FOR_ALL_TOPIC, + &drc721_events::ApprovalForAll { + owner: principal_value(1), + operator: principal_value(2), + approved: true, + }, + ); + assert_event_decodes( + drc721, + drc721_events::TOKEN_ROYALTY_SET_TOPIC, + &drc721_events::TokenRoyaltySet { + operator: principal_value(1), + token_id: 9, + receiver: principal_value(2), + basis_points: 500, + }, + ); + + let multisig = drivers.get("multisig_controller").unwrap(); + assert_output_decodes(multisig, "owners", &vec![principal_value(1)]); + assert_output_decodes(multisig, "threshold", &2u16); + assert_output_decodes(multisig, "proposal_ttl", &10u64); + assert_output_decodes(multisig, "tombstone_ttl", &5u64); + assert_output_decodes(multisig, "operation_id", &[7u8; 32]); + assert_output_decodes( + multisig, + "proposal", + &Some( + dusk_contract_standards::governance::MultisigPendingOperation { + target: multisig_target( + 2, + "set_value", + vec![1, 2, 3], + [4u8; 32], + ), + confirmations: vec![principal_value(1)], + deadline: 100, + }, + ), + ); + assert_output_decodes(multisig, "tombstone_expiry", &Some(120u64)); + assert_output_decodes(multisig, "propose", &vec![1u8, 2, 3]); + assert_output_decodes(multisig, "confirm", &vec![1u8, 2, 3]); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_PROPOSED_TOPIC, + &MultisigOperationProposed { + id: [1u8; 32], + authorizer: principal_value(1), + confirmations: 1, + threshold: 2, + deadline: 10, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_CONFIRMED_TOPIC, + &MultisigOperationConfirmed { + id: [1u8; 32], + authorizer: principal_value(2), + confirmations: 2, + threshold: 2, + deadline: 10, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_EXECUTED_TOPIC, + &MultisigOperationExecuted { + id: [1u8; 32], + success: true, + return_data: vec![9], + error: None, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_CANCELLED_TOPIC, + &MultisigOperationCancelled { + id: [1u8; 32], + signers: vec![principal_value(1), principal_value(2)], + }, + ); + assert_event_decodes( + multisig, + MULTISIG_AUTHORITY_UPDATED_TOPIC, + &MultisigAuthorityUpdated { + previous_owners: vec![principal_value(1)], + previous_threshold: 1, + owners: vec![principal_value(1), principal_value(2)], + threshold: 2, + removed_operations: vec![[3u8; 32]], + }, + ); + assert_event_decodes( + multisig, + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + &MultisigTimeLimitsUpdated { + previous_proposal_ttl: 10, + previous_tombstone_ttl: 5, + proposal_ttl: 20, + tombstone_ttl: 6, + }, + ); + + let proxy = drivers.get("proxy_counter").unwrap(); + assert_output_decodes(proxy, "implementation", &contract_id(4)); + assert_output_decodes(proxy, "rollback_deadline", &100u64); + assert_output_decodes(proxy, "activate_upgrade", &vec![1u8, 2, 3]); + assert_event_decodes( + proxy, + UPGRADE_PREPARED_TOPIC, + &UpgradePrepared { + implementation: contract_id(4), + eta: 10, + }, + ); + assert_event_decodes( + proxy, + UPGRADE_ACTIVATED_TOPIC, + &UpgradeActivated { + previous_implementation: contract_id(3), + implementation: contract_id(4), + rollback_deadline: 20, + }, + ); + assert_event_decodes( + proxy, + UPGRADE_CANCELLED_TOPIC, + &UpgradeCancelled { + implementation: contract_id(4), + }, + ); + assert_event_decodes( + proxy, + UPGRADE_ROLLED_BACK_TOPIC, + &UpgradeRolledBack { + from_implementation: contract_id(4), + restored_implementation: contract_id(3), + }, + ); + assert_event_decodes( + proxy, + ROLLBACK_FINALIZED_TOPIC, + &RollbackFinalized { + implementation: contract_id(4), + }, + ); + + for (name, driver) in &drivers { + assert!( + driver.decode_output_fn("definitely_missing", &[]).is_err(), + "{name} accepted an unknown output function" + ); + assert!( + driver.decode_event("definitely_missing", &[]).is_err(), + "{name} accepted an unknown event topic" + ); + } +} + +fn load_drivers() -> BTreeMap<&'static str, DriverReader> { + DRIVER_NAMES + .into_iter() + .map(|name| (name, load_driver(name))) + .collect() +} + +fn load_driver(name: &str) -> DriverReader { + let path = driver_path(name); + let wasm = fs::read(&path).unwrap_or_else(|error| { + panic!( + "failed to read {}: {error}; run `make standards-data-drivers`", + path.display() + ) + }); + DriverReader::new(&wasm) + .unwrap_or_else(|error| panic!("failed to load {name}: {error}")) +} + +fn driver_path(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/data-driver/wasm32-unknown-unknown/release") + .join(format!("{name}.wasm")) +} + +fn assert_rejects_bad_inputs(drivers: &BTreeMap<&'static str, DriverReader>) { + for (name, driver) in drivers { + assert!( + driver + .encode_input_fn("definitely_missing", "null") + .is_err(), + "{name} accepted an unknown function" + ); + assert!( + driver.encode_input_fn("value", "{").is_err(), + "{name} accepted malformed JSON" + ); + } + + let auth = drivers.get("authorization_counter").unwrap(); + assert!( + auth.encode_input_fn( + "nonce", + &json!({ + "principal": {"kind": "Phoenix", "bytes": [1]}, + "domain": bytes32(0), + }) + .to_string() + ) + .is_err(), + "authorization counter accepted a malformed Phoenix principal" + ); + + let drc20 = drivers.get("drc20_roles_pausable").unwrap(); + assert!( + drc20 + .encode_input_fn( + "balance_of", + &json!({"account": {"kind": "Moonlight", "bytes": bytes32(1)}}) + .to_string(), + ) + .is_err(), + "DRC20 accepted a short Moonlight principal" + ); + assert!( + drc20.encode_input_fn("burn", r#""not a number""#).is_err(), + "DRC20 accepted a string u64" + ); + + let drc721 = drivers.get("drc721_collection").unwrap(); + assert!( + drc721 + .encode_input_fn( + "set_approval_for_all", + &json!({"operator": principal(1), "approved": "yes"}) + .to_string(), + ) + .is_err(), + "DRC721 accepted a string bool" + ); +} + +fn assert_input_roundtrip( + driver: &DriverReader, + call: &CallSpec, +) -> Result<(), TestCaseError> { + let input = call.input.to_string(); + let encoded = + driver + .encode_input_fn(call.function, &input) + .map_err(|error| { + TestCaseError::fail(format!("encode failed: {error}")) + })?; + prop_assert!( + !encoded.is_empty() || call.input.is_null(), + "{}:{} encoded to an empty non-unit payload", + call.driver, + call.function + ); + + let decoded = + driver + .decode_input_fn(call.function, &encoded) + .map_err(|error| { + TestCaseError::fail(format!("decode failed: {error}")) + })?; + let reencoded = driver + .encode_input_fn(call.function, &decoded.to_string()) + .map_err(|error| { + TestCaseError::fail(format!("re-encode failed: {error}")) + })?; + let decoded_again = driver + .decode_input_fn(call.function, &reencoded) + .map_err(|error| { + TestCaseError::fail(format!( + "decode-after-reencode failed: {error}" + )) + })?; + + prop_assert_eq!(decoded_again, decoded); + Ok(()) +} + +fn assert_mutated_inputs_are_handled( + driver: &DriverReader, + call: &CallSpec, +) -> Result<(), TestCaseError> { + let encoded = driver + .encode_input_fn(call.function, &call.input.to_string()) + .map_err(|error| { + TestCaseError::fail(format!("encode failed: {error}")) + })?; + + for mutated in mutated_inputs(&encoded) { + if let Ok(decoded) = driver.decode_input_fn(call.function, &mutated) { + driver + .encode_input_fn(call.function, &decoded.to_string()) + .map_err(|error| { + TestCaseError::fail(format!( + "mutated input decoded to unencodable JSON: {error}" + )) + })?; + } + } + + Ok(()) +} + +fn assert_output_decodes(driver: &DriverReader, function: &str, value: &T) +where + T: rkyv::Serialize>, +{ + let encoded = rkyv_bytes(value); + driver + .decode_output_fn(function, &encoded) + .unwrap_or_else(|error| { + panic!("decode_output_fn({function}) failed: {error}") + }); + assert_mutated_output_decode_is_handled(driver, function, &encoded); +} + +fn assert_event_decodes(driver: &DriverReader, topic: &str, value: &T) +where + T: rkyv::Serialize>, +{ + let encoded = rkyv_bytes(value); + driver + .decode_event(topic, &encoded) + .unwrap_or_else(|error| { + panic!("decode_event({topic}) failed: {error}") + }); + assert_mutated_event_decode_is_handled(driver, topic, &encoded); +} + +fn assert_mutated_output_decode_is_handled( + driver: &DriverReader, + function: &str, + encoded: &[u8], +) { + for mutated in mutated_inputs(encoded) { + let _ = driver.decode_output_fn(function, &mutated); + } +} + +fn assert_mutated_event_decode_is_handled( + driver: &DriverReader, + topic: &str, + encoded: &[u8], +) { + for mutated in mutated_inputs(encoded) { + let _ = driver.decode_event(topic, &mutated); + } +} + +fn rkyv_bytes(value: &T) -> Vec +where + T: rkyv::Serialize>, +{ + rkyv::to_bytes::<_, 4096>(value) + .expect("rkyv serialization") + .to_vec() +} + +fn mutated_inputs(encoded: &[u8]) -> Vec> { + let mut mutations = Vec::new(); + mutations.push(Vec::new()); + + if !encoded.is_empty() { + let mut flipped = encoded.to_vec(); + flipped[0] ^= 0x80; + mutations.push(flipped); + + let mut middle = encoded.to_vec(); + let index = middle.len() / 2; + middle[index] ^= 0x55; + mutations.push(middle); + + let mut truncated = encoded.to_vec(); + truncated.truncate(truncated.len() / 2); + mutations.push(truncated); + } + + let mut appended = encoded.to_vec(); + appended.extend_from_slice(&[0xaa, 0x55, 0x00, 0xff]); + mutations.push(appended); + + mutations +} + +fn driver_case_strategy() -> impl Strategy { + ( + 0u8..5, + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map(|(contract, selector, a, b, amount, flag)| DriverCase { + contract: match contract { + 0 => ReferenceContract::AuthorizationCounter, + 1 => ReferenceContract::Drc20, + 2 => ReferenceContract::Drc721, + 3 => ReferenceContract::MultisigController, + _ => ReferenceContract::ProxyCounter, + }, + selector, + a, + b, + amount, + flag, + }) +} + +fn call_for(case: &DriverCase) -> CallSpec { + match case.contract { + ReferenceContract::AuthorizationCounter => auth_counter_call(case), + ReferenceContract::Drc20 => drc20_call(case), + ReferenceContract::Drc721 => drc721_call(case), + ReferenceContract::MultisigController => multisig_call(case), + ReferenceContract::ProxyCounter => proxy_call(case), + } +} + +fn auth_counter_call(case: &DriverCase) -> CallSpec { + match case.selector % 4 { + 0 => call("authorization_counter", "init", Value::Null), + 1 => call("authorization_counter", "value", Value::Null), + 2 => call("authorization_counter", "last_authorizer", Value::Null), + _ => call("authorization_counter", "nonce", nonce_query(case.a)), + } +} + +#[allow(clippy::too_many_lines)] +fn drc20_call(case: &DriverCase) -> CallSpec { + match case.selector % 27 { + 0 => call( + "drc20_roles_pausable", + "init", + json!({ + "admin": principal(case.a), + "token": { + "name": "Fuzz Token", + "symbol": "FUZZ", + "decimals": case.a % 19, + "initial_balances": [{ + "account": principal(case.b), + "amount": bounded(case.amount), + }], + }, + "cap": bounded(case.amount).saturating_add(1), + }), + ), + 1 => call("drc20_roles_pausable", "name", Value::Null), + 2 => call("drc20_roles_pausable", "symbol", Value::Null), + 3 => call("drc20_roles_pausable", "decimals", Value::Null), + 4 => call("drc20_roles_pausable", "total_supply", Value::Null), + 5 => call( + "drc20_roles_pausable", + "balance_of", + json!({"account": principal(case.a)}), + ), + 6 => call( + "drc20_roles_pausable", + "allowance", + json!({"owner": principal(case.a), "spender": principal(case.b)}), + ), + 7 => call("drc20_roles_pausable", "nonce", nonce_query(case.a)), + 8 => call("drc20_roles_pausable", "cap", Value::Null), + 9 => call("drc20_roles_pausable", "remaining_mintable", Value::Null), + 10 => call("drc20_roles_pausable", "latest_votes", principal(case.a)), + 11 => call( + "drc20_roles_pausable", + "past_votes", + json!({"account": principal(case.a), "timepoint": case.amount}), + ), + 12 => call("drc20_roles_pausable", "latest_total_votes", Value::Null), + 13 => call( + "drc20_roles_pausable", + "past_total_supply", + json!(case.amount), + ), + 14 => call( + "drc20_roles_pausable", + "has_role", + json!({"role": bytes32(case.a), "account": principal(case.b)}), + ), + 15 => call( + "drc20_roles_pausable", + "transfer", + json!({"to": principal(case.a), "amount": case.amount}), + ), + 16 => call( + "drc20_roles_pausable", + "approve", + json!({"spender": principal(case.a), "amount": case.amount}), + ), + 17 => call( + "drc20_roles_pausable", + "increase_allowance", + json!({"spender": principal(case.a), "added_amount": case.amount}), + ), + 18 => call( + "drc20_roles_pausable", + "decrease_allowance", + json!({ + "spender": principal(case.a), + "subtracted_amount": case.amount, + }), + ), + 19 => call( + "drc20_roles_pausable", + "transfer_from", + json!({ + "owner": principal(case.a), + "to": principal(case.b), + "amount": case.amount, + }), + ), + 20 => call( + "drc20_roles_pausable", + "mint", + json!({ + "to": principal(case.a), + "amount": case.amount, + "authorization": null, + }), + ), + 21 => call("drc20_roles_pausable", "burn", json!(case.amount)), + 22 => call( + "drc20_roles_pausable", + "pause", + json!({"authorization": null}), + ), + 23 => call( + "drc20_roles_pausable", + "unpause", + json!({"authorization": null}), + ), + 24 => call("drc20_roles_pausable", "paused", Value::Null), + 25 => call( + "drc20_roles_pausable", + "grant_role", + json!({ + "role": bytes32(case.a), + "account": principal(case.b), + "authorization": null, + }), + ), + _ => call( + "drc20_roles_pausable", + "revoke_role", + json!({ + "role": bytes32(case.a), + "account": principal(case.b), + "authorization": null, + }), + ), + } +} + +#[allow(clippy::too_many_lines)] +fn drc721_call(case: &DriverCase) -> CallSpec { + match case.selector % 27 { + 0 => call( + "drc721_collection", + "init", + json!({ + "owner": principal(case.a), + "token": { + "name": "Fuzz Collection", + "symbol": "FUZ", + "base_uri": "ipfs://fuzz/", + "initial_tokens": [{ + "account": principal(case.b), + "token_id": bounded(case.amount), + }], + }, + "default_royalty": royalty(case.a, case.amount), + }), + ), + 1 => call("drc721_collection", "name", Value::Null), + 2 => call("drc721_collection", "symbol", Value::Null), + 3 => call("drc721_collection", "base_uri", Value::Null), + 4 => call( + "drc721_collection", + "token_uri", + json!({"token_id": case.amount}), + ), + 5 => call( + "drc721_collection", + "token_by_index", + json!({"index": bounded(case.amount)}), + ), + 6 => call( + "drc721_collection", + "token_of_owner_by_index", + json!({"owner": principal(case.a), "index": bounded(case.amount)}), + ), + 7 => call( + "drc721_collection", + "tokens_of", + json!({"owner": principal(case.a)}), + ), + 8 => call("drc721_collection", "total_supply", Value::Null), + 9 => call( + "drc721_collection", + "balance_of", + json!({"account": principal(case.a)}), + ), + 10 => call( + "drc721_collection", + "owner_of", + json!({"token_id": case.amount}), + ), + 11 => call( + "drc721_collection", + "get_approved", + json!({"token_id": case.amount}), + ), + 12 => call( + "drc721_collection", + "is_approved_for_all", + json!({"owner": principal(case.a), "operator": principal(case.b)}), + ), + 13 => call("drc721_collection", "nonce", nonce_query(case.a)), + 14 => call( + "drc721_collection", + "royalty_info", + json!({"token_id": case.amount, "sale_price": bounded(case.amount)}), + ), + 15 => call( + "drc721_collection", + "approve", + json!({"approved": principal(case.a), "token_id": case.amount}), + ), + 16 => call( + "drc721_collection", + "set_approval_for_all", + json!({"operator": principal(case.a), "approved": case.flag}), + ), + 17 => call( + "drc721_collection", + "transfer_from", + json!({ + "from": principal(case.a), + "to": principal(case.b), + "token_id": case.amount, + }), + ), + 18 => call( + "drc721_collection", + "mint", + json!({ + "to": principal(case.a), + "token_id": case.amount, + "authorization": null, + }), + ), + 19 => call("drc721_collection", "burn", json!(case.amount)), + 20 => { + call("drc721_collection", "pause", json!({"authorization": null})) + } + 21 => call( + "drc721_collection", + "unpause", + json!({"authorization": null}), + ), + 22 => call("drc721_collection", "paused", Value::Null), + 23 => call( + "drc721_collection", + "set_default_royalty", + json!({"info": royalty(case.a, case.amount), "authorization": null}), + ), + 24 => call( + "drc721_collection", + "clear_default_royalty", + json!({"authorization": null}), + ), + 25 => call( + "drc721_collection", + "set_token_royalty", + json!({ + "token_id": case.amount, + "info": royalty(case.a, case.amount), + "authorization": null, + }), + ), + _ => call( + "drc721_collection", + "clear_token_royalty", + json!({"token_id": case.amount, "authorization": null}), + ), + } +} + +fn multisig_call(case: &DriverCase) -> CallSpec { + match case.selector % 14 { + 0 => call( + "multisig_controller", + "init", + json!({ + "config": { + "owners": [principal(case.a), principal(case.b)], + "threshold": 1u16, + "proposal_ttl": 10u64, + "tombstone_ttl": 5u64, + } + }), + ), + 1 => call("multisig_controller", "owners", Value::Null), + 2 => call("multisig_controller", "threshold", Value::Null), + 3 => call("multisig_controller", "proposal_ttl", Value::Null), + 4 => call("multisig_controller", "tombstone_ttl", Value::Null), + 5 => call("multisig_controller", "nonce", nonce_query(case.a)), + 6 => call( + "multisig_controller", + "operation_id", + multisig_target_json(case.a, case.b), + ), + 7 => call("multisig_controller", "proposal", json!(bytes32(case.a))), + 8 => call( + "multisig_controller", + "tombstone_expiry", + json!(bytes32(case.a)), + ), + 9 => call( + "multisig_controller", + "propose", + json!({ + "target": multisig_target_json(case.a, case.b), + "authorization": null, + }), + ), + 10 => call( + "multisig_controller", + "confirm", + json!({"id": bytes32(case.a), "authorization": null}), + ), + 11 => call( + "multisig_controller", + "cancel", + json!({"id": bytes32(case.a), "approvals": {"approvals": []}}), + ), + 12 => call( + "multisig_controller", + "update_authority", + json!({ + "owners": [principal(case.a)], + "threshold": 1u16, + "approvals": {"approvals": []}, + }), + ), + _ => call( + "multisig_controller", + "set_time_limits", + json!({ + "proposal_ttl": bounded(case.amount).saturating_add(1), + "tombstone_ttl": 5u64, + "approvals": {"approvals": []}, + }), + ), + } +} + +fn proxy_call(case: &DriverCase) -> CallSpec { + match case.selector % 9 { + 0 => call("proxy_counter", "implementation", Value::Null), + 1 => call("proxy_counter", "rollback_deadline", Value::Null), + 2 => call("proxy_counter", "nonce", nonce_query(case.a)), + 3 => call("proxy_counter", "value", Value::Null), + 4 => call("proxy_counter", "increment", Value::Null), + 5 => call( + "proxy_counter", + "set_value", + json!({"value": case.amount, "authorization": null}), + ), + 6 => call( + "proxy_counter", + "activate_upgrade", + json!({"authorization": null}), + ), + 7 => call( + "proxy_counter", + "cancel_pending_upgrade", + json!({"authorization": null}), + ), + _ => call( + "proxy_counter", + "finalize_rollback_window", + json!({"authorization": null}), + ), + } +} + +fn call( + driver: &'static str, + function: &'static str, + input: Value, +) -> CallSpec { + CallSpec { + driver, + function, + input, + } +} + +fn nonce_query(seed: u8) -> Value { + json!({"principal": principal(seed), "domain": bytes32(seed)}) +} + +fn principal(seed: u8) -> Value { + match seed % 3 { + 0 => json!({"kind": "Phoenix", "bytes": bytes32(seed)}), + 1 => json!({"kind": "Contract", "bytes": bytes32(seed)}), + _ => json!({"kind": "Moonlight", "bytes": bytes193(seed)}), + } +} + +fn multisig_target_json(contract_seed: u8, salt_seed: u8) -> Value { + json!({ + "call": { + "contract": hex_bytes(contract_seed, 32), + "fn_name": "set_value", + "fn_args": "", + }, + "salt": bytes32(salt_seed), + }) +} + +fn multisig_target( + contract_seed: u8, + function: &str, + args: Vec, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(contract_id(contract_seed), function) + .with_raw_args(args), + salt, + } +} + +fn royalty(seed: u8, amount: u64) -> Value { + json!({ + "receiver": principal(seed), + "basis_points": (amount % 10_001) as u16, + }) +} + +fn principal_value(seed: u8) -> Principal { + match seed % 3 { + 0 => Principal::Phoenix(bytes32_array(seed)), + 1 => Principal::Contract(contract_id(seed)), + _ => Principal::Moonlight(bytes193_array(seed)), + } +} + +fn contract_id(seed: u8) -> ContractId { + ContractId::from_bytes(bytes32_array(seed)) +} + +fn bytes32(seed: u8) -> Vec { + (0..32).map(|index| seed.wrapping_add(index)).collect() +} + +fn bytes32_array(seed: u8) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = seed.wrapping_add(index as u8); + } + bytes +} + +fn bytes193(seed: u8) -> Vec { + (0..193).map(|index| seed.wrapping_add(index)).collect() +} + +fn bytes193_array(seed: u8) -> [u8; 193] { + let mut bytes = [0u8; 193]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = seed.wrapping_add(index as u8); + } + bytes +} + +fn hex_bytes(seed: u8, len: usize) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(len * 2); + for byte in (0..len).map(|index| seed.wrapping_add(index as u8)) { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn bounded(value: u64) -> u64 { + value % 1_000_000 +} + +fn env_usize(name: &str) -> Option { + std::env::var(name).ok()?.parse().ok() +} diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs new file mode 100644 index 00000000..edf421f5 --- /dev/null +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -0,0 +1,3040 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::fs; +use std::path::{Path, PathBuf}; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::governance::{ + MultisigControllerConfig, MultisigOperationExecuted, MultisigOperationId, + MultisigPendingOperation, MultisigTarget, + MULTISIG_OPERATION_EXECUTED_TOPIC, +}; +use dusk_contract_standards::token::drc20::{ + Allowance, BalanceOf as BalanceOf20, Init as Init20, InitBalance, + SignedApproveCall, TransferCall as TransferCall20, + TransferFromCall as TransferFromCall20, +}; +use dusk_contract_standards::token::drc721::{ + GetApproved, Init as Init721, InitToken, IsApprovedForAll, OwnerOf, + RoyaltyInfo, SignedApproveCall as SignedNftApproveCall, + SignedSetApprovalForAllCall, TokensOf, + TransferFromCall as TransferFromCall721, +}; +use dusk_core::abi::{ContractId, Metadata}; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::transfer::data::ContractCall; +use dusk_core::transfer::TRANSFER_CONTRACT; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use dusk_vm::{ContractData, Session, VM}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rkyv::{Archive, Deserialize, Serialize}; + +const CHAIN_ID: u8 = 0xD5; +const GAS_LIMIT: u64 = 1_000_000_000; +const OWNER_BYTES: [u8; 32] = [1u8; 32]; +const SET_VALUE_DOMAIN: NonceDomain = [3u8; 32]; +const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; +const TOKEN_ADMIN_DOMAIN: NonceDomain = [11u8; 32]; +const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +const MINT_ACTION: [u8; 32] = [13u8; 32]; +const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; +const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; +const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; +const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; +const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const NFT_SET_DEFAULT_ROYALTY_ACTION: [u8; 32] = [25u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; +const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; +const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; +const MULTISIG_CONTROLLER_DOMAIN: NonceDomain = [41u8; 32]; +const MULTISIG_PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +const MULTISIG_CONFIRM_ACTION: [u8; 32] = [43u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct SetValueByMoonlight { + authorization: MoonlightAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization, + amount: u64, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +struct NonceQuery { + principal: Principal, + domain: NonceDomain, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20ExampleInit { + admin: Principal, + token: Init20, + cap: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20MintCall { + to: Principal, + amount: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct ForwardCall { + contract: ContractId, + function: String, + args: Vec, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721ExampleInit { + owner: Principal, + token: Init721, + default_royalty: + Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721MintCall { + to: Principal, + token_id: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721SetDefaultRoyaltyCall { + info: RoyaltyInfo, + authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +struct ProxyCounterInit { + admin: Principal, + implementation: ContractId, + upgrade_delay: u64, + rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct ProxySetValue { + value: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigInit { + config: MultisigControllerConfig, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigPropose { + target: MultisigTarget, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigConfirm { + id: MultisigOperationId, + authorization: Option, +} + +#[test] +#[ignore = "requires release Wasm examples; run after the wasm build"] +fn wasm_examples_deploy_and_answer_queries() { + let _hard_fork_guard = + host_queries::set_hard_fork(host_queries::HardFork::Aegis); + let vm = VM::ephemeral().expect("create ephemeral VM"); + let mut session = vm.genesis_session(CHAIN_ID); + + let admin = phoenix_principal(7); + let moonlight_router = deploy( + &mut session, + "moonlight_call_router.wasm", + TRANSFER_CONTRACT.to_bytes(), + &(), + ); + let auth_counter = + deploy(&mut session, "authorization_counter.wasm", [19u8; 32], &()); + let auth_value: u64 = session + .call(auth_counter, "value", &(), GAS_LIMIT) + .expect("query authorization counter value") + .data; + assert_eq!(auth_value, 0); + exercise_authorization_counter_signed_calls(&mut session, auth_counter); + + let drc20 = deploy( + &mut session, + "drc20_roles_pausable.wasm", + [20u8; 32], + &Drc20ExampleInit { + admin, + token: Init20 { + name: "VM Dusk Token".into(), + symbol: "VDUSK".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: admin, + amount: 1_000, + }], + }, + cap: 10_000, + }, + ); + let drc20_supply: u64 = session + .call(drc20, "total_supply", &(), GAS_LIMIT) + .expect("query drc20 total supply") + .data; + assert_eq!(drc20_supply, 1_000); + let drc20_balance: u64 = session + .call( + drc20, + "balance_of", + &BalanceOf20 { account: admin }, + GAS_LIMIT, + ) + .expect("query drc20 balance") + .data; + assert_eq!(drc20_balance, 1_000); + assert!(session + .call::<_, ()>( + drc20, + "mint", + &Drc20MintCall { + to: admin, + amount: 1, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + drc20, + "pause", + &Drc20AdminCall { + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_drc20_signed_calls(&mut session, drc20, admin); + let moonlight_owner_sk = moonlight_secret(110); + let moonlight_owner_pk = BlsPublicKey::from(&moonlight_owner_sk); + let moonlight_owner = Principal::moonlight(&moonlight_owner_pk); + let moonlight_drc20 = deploy( + &mut session, + "drc20_roles_pausable.wasm", + [25u8; 32], + &Drc20ExampleInit { + admin, + token: Init20 { + name: "VM Moonlight Token".into(), + symbol: "VML".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: moonlight_owner, + amount: 100, + }], + }, + cap: 1_000, + }, + ); + exercise_drc20_moonlight_observed_caller( + &mut session, + moonlight_router, + moonlight_drc20, + moonlight_owner_pk, + moonlight_owner, + admin, + ); + + let drc721 = deploy( + &mut session, + "drc721_collection.wasm", + [21u8; 32], + &Drc721ExampleInit { + owner: admin, + token: Init721 { + name: "VM Dusk Collection".into(), + symbol: "VDNFT".into(), + base_uri: "ipfs://vm/".into(), + initial_tokens: vec![InitToken { + account: admin, + token_id: 42, + }], + }, + default_royalty: None, + }, + ); + let drc721_supply: u64 = session + .call(drc721, "total_supply", &(), GAS_LIMIT) + .expect("query drc721 total supply") + .data; + assert_eq!(drc721_supply, 1); + let owner: Principal = session + .call(drc721, "owner_of", &OwnerOf { token_id: 42 }, GAS_LIMIT) + .expect("query drc721 owner") + .data; + assert_eq!(owner, admin); + let tokens: Vec = session + .call(drc721, "tokens_of", &TokensOf { owner: admin }, GAS_LIMIT) + .expect("query drc721 tokens") + .data; + assert_eq!(tokens, vec![42]); + assert!(session + .call::<_, ()>( + drc721, + "mint", + &Drc721MintCall { + to: admin, + token_id: 43, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + drc721, + "pause", + &Drc721AdminCall { + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_drc721_signed_calls(&mut session, drc721, admin); + + let proxy = deploy( + &mut session, + "proxy_counter.wasm", + [22u8; 32], + &ProxyCounterInit { + admin, + implementation: ContractId::from_bytes([99u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + }, + ); + let value: u64 = session + .call(proxy, "value", &(), GAS_LIMIT) + .expect("query proxy value") + .data; + assert_eq!(value, 0); + let implementation: ContractId = session + .call(proxy, "implementation", &(), GAS_LIMIT) + .expect("query proxy implementation") + .data; + assert_eq!(implementation, ContractId::from_bytes([99u8; 32])); + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 7, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_proxy_signed_admin_call(&mut session, proxy, admin); + + let multisig_owner_a = phoenix_principal(8); + let multisig_owner_b = phoenix_principal(9); + let multisig_owner_c = phoenix_principal(10); + let multisig = deploy( + &mut session, + "multisig_controller.wasm", + [23u8; 32], + &MultisigInit { + config: MultisigControllerConfig { + owners: vec![ + multisig_owner_a, + multisig_owner_b, + multisig_owner_c, + ], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 6, + }, + }, + ); + let governed_proxy = deploy( + &mut session, + "proxy_counter.wasm", + [24u8; 32], + &ProxyCounterInit { + admin: Principal::contract(multisig), + implementation: ContractId::from_bytes([101u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + }, + ); + exercise_multisig_controller_admin_call( + &mut session, + multisig, + governed_proxy, + multisig_owner_a, + multisig_owner_b, + multisig_owner_c, + ); +} + +fn exercise_drc20_signed_calls( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(2222); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + assert_eq!(admin, Principal::phoenix_public_key(&admin_pk)); + + let mint_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 0, + 0, + mint_payload_hash(admin, 5), + ); + let mint_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + mint_action, + )); + session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 5, + authorization: Some(mint_auth.clone()), + }, + GAS_LIMIT, + ) + .expect("mint by signed Phoenix admin"); + let supply: u64 = session + .call(contract, "total_supply", &(), GAS_LIMIT) + .expect("query drc20 supply") + .data; + assert_eq!(supply, 1_005); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 5, + authorization: Some(mint_auth), + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_mint_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 9), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_mint_action, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let wrong_contract_mint = authorized_action( + ContractId::from_bytes([77u8; 32]), + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let wrong_action_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + [0xab; 32], + 1, + 0, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let expired_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 9, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired_mint), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let cap_exceeded_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 10_000), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 10_000, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + cap_exceeded_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let zero = Principal::Contract(ContractId::from_bytes([0u8; 32])); + let zero_recipient_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(zero, 1), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: zero, + amount: 1, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + zero_recipient_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let spender_sk = moonlight_secret(30); + let spender_pk = BlsPublicKey::from(&spender_sk); + let spender = Principal::moonlight(&spender_pk); + + let approve_action = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 0, + 0, + approve_payload_hash(admin, spender, 33), + ); + let approve_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + approve_action, + )); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 33, + authorization: approve_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("approve by Phoenix authorization"); + let allowance: u64 = session + .call( + contract, + "allowance", + &Allowance { + owner: admin, + spender, + }, + GAS_LIMIT, + ) + .expect("query signed allowance") + .data; + assert_eq!(allowance, 33); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 33, + authorization: approve_auth, + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_approve_payload = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 1, + 0, + approve_payload_hash(admin, spender, 34), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 35, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_approve_payload, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + let allowance: u64 = session + .call( + contract, + "allowance", + &Allowance { + owner: admin, + spender, + }, + GAS_LIMIT, + ) + .expect("query signed allowance after bad payload") + .data; + assert_eq!(allowance, 33); + + let expired_approve = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 1, + 9, + approve_payload_hash(admin, spender, 34), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 34, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_approve, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + + let moonlight_owner_sk = moonlight_secret(31); + let moonlight_owner_pk = BlsPublicKey::from(&moonlight_owner_sk); + let moonlight_owner = Principal::moonlight(&moonlight_owner_pk); + let moonlight_action = authorized_action( + contract, + moonlight_owner, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 0, + 0, + approve_payload_hash(moonlight_owner, admin, 44), + ); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: moonlight_owner, + spender: admin, + amount: 44, + authorization: SignedAuthorization::Moonlight(moonlight_auth( + &moonlight_owner_sk, + moonlight_owner_pk, + moonlight_action, + )), + }, + GAS_LIMIT, + ) + .expect("approve by Moonlight authorization"); + assert_contract_nonce( + session, + contract, + moonlight_owner, + SIGNED_APPROVE_DOMAIN, + 1, + ); + + let pause_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + 1, + 0, + empty_payload_hash(b"drc20.pause"), + ); + session + .call::<_, ()>( + contract, + "pause", + &Drc20AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, pause_action), + )), + }, + GAS_LIMIT, + ) + .expect("pause by signed Phoenix admin"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc20 paused") + .data; + assert!(paused); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + let duplicate_pause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc20.pause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "pause", + &Drc20AdminCall { + authorization: Some(duplicate_pause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + assert!(session + .call::<_, ()>( + contract, + "transfer", + &TransferCall20 { + to: admin, + amount: 1, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + contract, + "transfer_from", + &TransferFromCall20 { + owner: admin, + to: admin, + amount: 1, + }, + GAS_LIMIT, + ) + .is_err()); + + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 2, + 0, + mint_payload_hash(admin, 6), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 6, + authorization: Some(paused_mint), + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>(contract, "burn", &1u64, GAS_LIMIT) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + let unpause_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc20.unpause"), + ); + session + .call::<_, ()>( + contract, + "unpause", + &Drc20AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, unpause_action), + )), + }, + GAS_LIMIT, + ) + .expect("unpause by signed Phoenix admin"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc20 unpaused") + .data; + assert!(!paused); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); + + let duplicate_unpause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + 3, + 0, + empty_payload_hash(b"drc20.unpause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "unpause", + &Drc20AdminCall { + authorization: Some(duplicate_unpause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); +} + +fn exercise_drc20_moonlight_observed_caller( + session: &mut Session, + router: ContractId, + contract: ContractId, + moonlight_pk: BlsPublicKey, + moonlight: Principal, + admin: Principal, +) { + assert_drc20_balance(session, contract, moonlight, 100); + assert_drc20_balance(session, contract, admin, 0); + + let transfer = TransferCall20 { + to: admin, + amount: 25, + }; + assert!(session + .call::<_, ()>(contract, "transfer", &transfer, GAS_LIMIT) + .is_err()); + assert_drc20_balance(session, contract, moonlight, 100); + assert_drc20_balance(session, contract, admin, 0); + + session + .set_meta(Metadata::PUBLIC_SENDER, Some(moonlight_pk)) + .expect("set public sender"); + session + .call::<_, Vec>( + router, + "forward", + &ForwardCall { + contract, + function: "transfer".to_string(), + args: Session::serialize_data(&transfer) + .expect("serialize routed transfer"), + }, + GAS_LIMIT, + ) + .expect("route transfer with observed Moonlight caller"); + let _ = session.remove_meta(Metadata::PUBLIC_SENDER); + assert_drc20_balance(session, contract, moonlight, 75); + assert_drc20_balance(session, contract, admin, 25); + + let outsider_pk = BlsPublicKey::from(&moonlight_secret(111)); + session + .set_meta(Metadata::PUBLIC_SENDER, Some(outsider_pk)) + .expect("set outsider public sender"); + assert!(session + .call::<_, Vec>( + router, + "forward", + &ForwardCall { + contract, + function: "transfer".to_string(), + args: Session::serialize_data(&TransferCall20 { + to: admin, + amount: 1, + }) + .expect("serialize routed transfer"), + }, + GAS_LIMIT, + ) + .is_err()); + let _ = session.remove_meta(Metadata::PUBLIC_SENDER); + assert_drc20_balance(session, contract, moonlight, 75); + assert_drc20_balance(session, contract, admin, 25); +} + +fn exercise_drc721_signed_calls( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(4444); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + assert_eq!(admin, Principal::phoenix_public_key(&admin_pk)); + + let mint_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 0, + 0, + nft_mint_payload_hash(admin, 43), + ); + session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 43, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, mint_action), + )), + }, + GAS_LIMIT, + ) + .expect("mint NFT by signed Phoenix owner"); + let owner: Principal = session + .call(contract, "owner_of", &OwnerOf { token_id: 43 }, GAS_LIMIT) + .expect("query signed NFT mint owner") + .data; + assert_eq!(owner, admin); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let wrong_contract_mint = authorized_action( + ContractId::from_bytes([78u8; 32]), + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 0, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let wrong_action_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + [0xac; 32], + 1, + 0, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_mint + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let expired_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 9, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired_mint), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let zero = Principal::Contract(ContractId::from_bytes([0u8; 32])); + let zero_recipient_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 0, + nft_mint_payload_hash(zero, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: zero, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + zero_recipient_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let approved = Principal::Contract(ContractId::from_bytes([55u8; 32])); + let approve_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 0, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + let approve_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + approve_action, + )); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: approve_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("approve NFT by signed Phoenix owner"); + let actual_approved: Principal = session + .call( + contract, + "get_approved", + &GetApproved { token_id: 43 }, + GAS_LIMIT, + ) + .expect("query signed NFT approval") + .data; + assert_eq!(actual_approved, approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: approve_auth, + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_payload_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 0, + nft_approve_payload_hash(admin, approved, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_payload_action, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let wrong_contract_approval = authorized_action( + ContractId::from_bytes([79u8; 32]), + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let wrong_action_approval = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + [0xad; 32], + 1, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let expired_approval = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 9, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let operator = Principal::Contract(ContractId::from_bytes([56u8; 32])); + let operator_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 1, + 0, + nft_approval_for_all_payload_hash(admin, operator, true), + ); + let operator_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + operator_action, + )); + session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: true, + authorization: operator_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("set NFT operator approval by signed Phoenix owner"); + let operator_approved: bool = session + .call( + contract, + "is_approved_for_all", + &IsApprovedForAll { + owner: admin, + operator, + }, + GAS_LIMIT, + ) + .expect("query signed NFT operator approval") + .data; + assert!(operator_approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + assert!(session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: true, + authorization: operator_auth, + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + + let bad_operator_payload = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, + 0, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + assert!(session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: true, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_operator_payload, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + + let wrong_action_operator = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + [0xae; 32], + 2, + 0, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + assert!(session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: false, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_operator, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + + let expired_operator = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, + 9, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + assert!(session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: false, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_operator, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + + let invalid_royalty = RoyaltyInfo { + receiver: admin, + basis_points: 10_001, + }; + let invalid_royalty_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_SET_DEFAULT_ROYALTY_ACTION, + 1, + 0, + royalty_payload_hash(None, invalid_royalty), + ); + assert!(session + .call::<_, ()>( + contract, + "set_default_royalty", + &Drc721SetDefaultRoyaltyCall { + info: invalid_royalty, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + invalid_royalty_action, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let pause_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_PAUSE_ACTION, + 1, + 0, + empty_payload_hash(b"drc721.pause"), + ); + session + .call::<_, ()>( + contract, + "pause", + &Drc721AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, pause_action), + )), + }, + GAS_LIMIT, + ) + .expect("pause NFT by signed Phoenix owner"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc721 paused") + .data; + assert!(paused); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + let duplicate_pause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_PAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc721.pause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "pause", + &Drc721AdminCall { + authorization: Some(duplicate_pause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + assert!(session + .call::<_, ()>( + contract, + "transfer_from", + &TransferFromCall721 { + from: admin, + to: admin, + token_id: 43, + }, + GAS_LIMIT, + ) + .is_err()); + + let paused_operator_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, + 0, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: false, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + paused_operator_action, + )), + }, + GAS_LIMIT, + ) + .expect("signed NFT operator approval remains available while paused"); + let operator_approved: bool = session + .call( + contract, + "is_approved_for_all", + &IsApprovedForAll { + owner: admin, + operator, + }, + GAS_LIMIT, + ) + .expect("query paused signed NFT operator approval") + .data; + assert!(!operator_approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 3, + ); + + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 2, + 0, + nft_mint_payload_hash(admin, 44), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(paused_mint), + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>(contract, "burn", &43u64, GAS_LIMIT) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + let unpause_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_UNPAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc721.unpause"), + ); + session + .call::<_, ()>( + contract, + "unpause", + &Drc721AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, unpause_action), + )), + }, + GAS_LIMIT, + ) + .expect("unpause NFT by signed Phoenix owner"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc721 unpaused") + .data; + assert!(!paused); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 3); + + let duplicate_unpause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_UNPAUSE_ACTION, + 3, + 0, + empty_payload_hash(b"drc721.unpause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "unpause", + &Drc721AdminCall { + authorization: Some(duplicate_unpause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 3); +} + +fn exercise_proxy_signed_admin_call( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(3333); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + let action = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 0, + 0, + proxy_value_payload_hash(7), + ); + let auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &admin_sk, admin_pk, action, + )); + session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 7, + authorization: Some(auth.clone()), + }, + GAS_LIMIT, + ) + .expect("set proxy value by signed admin"); + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value after signed set") + .data; + assert_eq!(value, 7); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 7, + authorization: Some(auth), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value after replay") + .data; + assert_eq!(value, 7); + + let wrong_payload = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 9, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_payload), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let wrong_contract = authorized_action( + ContractId::from_bytes([80u8; 32]), + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_contract), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let wrong_action = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + [0xaf; 32], + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_action), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let expired = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 9, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); +} + +fn exercise_multisig_controller_admin_call( + session: &mut Session, + multisig: ContractId, + proxy: ContractId, + owner_a: Principal, + owner_b: Principal, + owner_c: Principal, +) { + let mut rng = StdRng::seed_from_u64(5555); + let owner_a_sk = SchnorrSecretKey::from(JubJubScalar::from(8u64)); + let owner_a_pk = SchnorrPublicKey::from(&owner_a_sk); + let owner_b_sk = SchnorrSecretKey::from(JubJubScalar::from(9u64)); + let owner_b_pk = SchnorrPublicKey::from(&owner_b_sk); + let owner_c_sk = SchnorrSecretKey::from(JubJubScalar::from(10u64)); + let owner_c_pk = SchnorrPublicKey::from(&owner_c_sk); + let outsider = phoenix_principal(11); + let outsider_sk = SchnorrSecretKey::from(JubJubScalar::from(11u64)); + let outsider_pk = SchnorrPublicKey::from(&outsider_sk); + assert_eq!(owner_a, Principal::phoenix_public_key(&owner_a_pk)); + assert_eq!(owner_b, Principal::phoenix_public_key(&owner_b_pk)); + assert_eq!(owner_c, Principal::phoenix_public_key(&owner_c_pk)); + assert_eq!(outsider, Principal::phoenix_public_key(&outsider_pk)); + + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 55, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert_proxy_value(session, proxy, 0); + + let direct_owner_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + proxy, + owner_a, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 0, + 0, + proxy_value_payload_hash(66), + ), + )); + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 66, + authorization: Some(direct_owner_auth), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, proxy, owner_a, PROXY_ADMIN_DOMAIN, 0); + assert_proxy_value(session, proxy, 0); + + let target = proxy_set_value_target(proxy, 77, [1u8; 32]); + let id: MultisigOperationId = session + .call(multisig, "operation_id", &target, GAS_LIMIT) + .expect("compute multisig operation id") + .data; + + let outsider_propose = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &outsider_sk, + outsider_pk, + authorized_action( + multisig, + outsider, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 0, + 0, + id, + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "propose", + &MultisigPropose { + target: target.clone(), + authorization: Some(outsider_propose), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + outsider, + MULTISIG_CONTROLLER_DOMAIN, + 0, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query rejected multisig operation") + .data; + assert!(pending.is_none()); + + let propose_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 0, + 0, + id, + ), + )); + let returned: Vec = session + .call( + multisig, + "propose", + &MultisigPropose { + target: target.clone(), + authorization: Some(propose_auth), + }, + GAS_LIMIT, + ) + .expect("propose multisig operation") + .data; + assert!(returned.is_empty()); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query pending multisig operation") + .data; + assert_eq!(pending.expect("pending").confirmations, vec![owner_a]); + assert_proxy_value(session, proxy, 0); + + let duplicate_confirm = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 1, + 0, + id, + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(duplicate_confirm), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + + let wrong_payload = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_b_sk, + owner_b_pk, + authorized_action( + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + [0xee; 32], + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(wrong_payload), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 0, + ); + + let confirm_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_b_sk, + owner_b_pk, + authorized_action( + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + id, + ), + )); + let receipt = session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(confirm_auth.clone()), + }, + GAS_LIMIT, + ) + .expect("confirm and execute multisig operation"); + let execution = receipt + .events + .iter() + .find(|event| event.topic == MULTISIG_OPERATION_EXECUTED_TOPIC) + .map(|event| { + rkyv::from_bytes::(&event.data) + .expect("decode multisig execution event") + }) + .expect("multisig execution event"); + assert!( + execution.success, + "multisig target execution failed: {:?}", + execution.error + ); + assert_proxy_value(session, proxy, 77); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query executed multisig operation") + .data; + assert!(pending.is_none()); + let tombstone: Option = session + .call(multisig, "tombstone_expiry", &id, GAS_LIMIT) + .expect("query multisig tombstone") + .data; + assert!(tombstone.is_some()); + + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(confirm_auth), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + assert_proxy_value(session, proxy, 77); + + let second_target = proxy_set_value_target(proxy, 88, [2u8; 32]); + let second_id: MultisigOperationId = session + .call(multisig, "operation_id", &second_target, GAS_LIMIT) + .expect("compute second multisig operation id") + .data; + let second_propose_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 1, + 0, + second_id, + ), + )); + let returned: Vec = session + .call( + multisig, + "propose", + &MultisigPropose { + target: second_target, + authorization: Some(second_propose_auth), + }, + GAS_LIMIT, + ) + .expect("propose second multisig operation") + .data; + assert!(returned.is_empty()); + assert_proxy_value(session, proxy, 77); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 2, + ); + + let second_confirm_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_c_sk, + owner_c_pk, + authorized_action( + multisig, + owner_c, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + second_id, + ), + )); + let receipt = session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id: second_id, + authorization: Some(second_confirm_auth), + }, + GAS_LIMIT, + ) + .expect("confirm second multisig operation"); + let execution = receipt + .events + .iter() + .find(|event| event.topic == MULTISIG_OPERATION_EXECUTED_TOPIC) + .map(|event| { + rkyv::from_bytes::(&event.data) + .expect("decode second multisig execution event") + }) + .expect("second multisig execution event"); + assert!( + execution.success, + "second multisig target execution failed: {:?}", + execution.error + ); + assert_proxy_value(session, proxy, 88); + assert_contract_nonce( + session, + multisig, + owner_c, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); +} + +fn exercise_authorization_counter_signed_calls( + session: &mut Session, + contract: ContractId, +) { + let moonlight_sk = moonlight_secret(7); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight = Principal::moonlight(&moonlight_pk); + + let moonlight_action = + counter_action(contract, moonlight, 0, 0, SET_VALUE_ACTION, 11); + let moonlight_auth_0 = + moonlight_auth(&moonlight_sk, moonlight_pk, moonlight_action); + session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth_0.clone(), + amount: 11, + }, + GAS_LIMIT, + ) + .expect("set counter by Moonlight signature"); + assert_counter(session, contract, 11, Some(moonlight), moonlight, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth_0, + amount: 11, + }, + GAS_LIMIT, + ) + .is_err()); + assert_counter(session, contract, 11, Some(moonlight), moonlight, 1); + + let bad_payload_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + bad_payload_action, + ), + amount: 13, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + let valid_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + let wrong_moonlight_sk = moonlight_secret(8); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: MoonlightAuthorization { + action: valid_action, + public_key: moonlight_pk, + signature: wrong_moonlight_sk + .sign(&valid_action.message_bytes()), + }, + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + let wrong_contract_action = counter_action( + ContractId::from_bytes([99u8; 32]), + moonlight, + 1, + 0, + SET_VALUE_ACTION, + 12, + ); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_contract_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + + let mut wrong_chain_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + wrong_chain_action.chain_id = CHAIN_ID.wrapping_add(1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_chain_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + + let wrong_action = counter_action(contract, moonlight, 1, 0, [9u8; 32], 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + + session + .set_meta(Metadata::BLOCK_HEIGHT, 10u64) + .expect("set VM block height"); + let expired_action = + counter_action(contract, moonlight, 1, 9, SET_VALUE_ACTION, 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + expired_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + valid_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .expect("set counter by second Moonlight signature"); + assert_counter(session, contract, 12, Some(moonlight), moonlight, 2); + + let mut rng = StdRng::seed_from_u64(1234); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + + let phoenix_action = + counter_action(contract, phoenix, 0, 0, SET_VALUE_ACTION, 21); + let phoenix_auth_0 = + phoenix_auth(&mut rng, &phoenix_sk, phoenix_pk, phoenix_action); + session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth_0.clone(), + amount: 21, + }, + GAS_LIMIT, + ) + .expect("set counter by Phoenix signature"); + assert_counter(session, contract, 21, Some(phoenix), phoenix, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth_0, + amount: 21, + }, + GAS_LIMIT, + ) + .is_err()); + assert_counter(session, contract, 21, Some(phoenix), phoenix, 1); + + let bad_payload_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + bad_payload_action, + ), + amount: 23, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let valid_phoenix_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + let wrong_phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(89u64)); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization { + action: valid_phoenix_action, + public_key: phoenix_pk, + signature: wrong_phoenix_sk + .sign(&mut rng, valid_phoenix_action.message_hash()), + replay_key: None, + }, + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let wrong_phoenix_pk = SchnorrPublicKey::from(&wrong_phoenix_sk); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &wrong_phoenix_sk, + wrong_phoenix_pk, + valid_phoenix_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let wrong_contract_action = counter_action( + ContractId::from_bytes([98u8; 32]), + phoenix, + 1, + 0, + SET_VALUE_ACTION, + 22, + ); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_contract_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + + let mut wrong_chain_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + wrong_chain_action.chain_id = CHAIN_ID.wrapping_add(1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_chain_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + + let wrong_action = counter_action(contract, phoenix, 1, 0, [8u8; 32], 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + + let expired_action = + counter_action(contract, phoenix, 1, 9, SET_VALUE_ACTION, 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + expired_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + valid_phoenix_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .expect("set counter by second Phoenix signature"); + assert_counter(session, contract, 22, Some(phoenix), phoenix, 2); +} + +fn counter_action( + contract: ContractId, + principal: Principal, + nonce: u64, + expires_at: u64, + action_id: [u8; 32], + amount: u64, +) -> AuthorizedAction { + authorized_action( + contract, + principal, + SET_VALUE_DOMAIN, + action_id, + nonce, + expires_at, + amount_hash(amount), + ) +} + +fn authorized_action( + contract: ContractId, + principal: Principal, + domain: NonceDomain, + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +) -> AuthorizedAction { + AuthorizedAction { + chain_id: CHAIN_ID, + contract, + domain, + action_id, + nonce, + expires_at, + principal, + payload_hash, + } +} + +fn moonlight_auth( + secret_key: &BlsSecretKey, + public_key: BlsPublicKey, + action: AuthorizedAction, +) -> MoonlightAuthorization { + let signature = secret_key.sign(&action.message_bytes()); + assert!(host_queries::verify_bls( + action.message_bytes(), + public_key, + signature + )); + MoonlightAuthorization { + action, + public_key, + signature, + } +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn phoenix_auth( + rng: &mut StdRng, + secret_key: &SchnorrSecretKey, + public_key: SchnorrPublicKey, + action: AuthorizedAction, +) -> PhoenixSignatureAuthorization { + PhoenixSignatureAuthorization { + action, + public_key, + signature: secret_key.sign(rng, action.message_hash()), + replay_key: None, + } +} + +fn assert_counter( + session: &mut Session, + contract: ContractId, + value: u64, + last_authorizer: Option, + principal: Principal, + nonce: u64, +) { + let actual_value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query authorization counter value") + .data; + assert_eq!(actual_value, value); + let actual_authorizer: Option = session + .call(contract, "last_authorizer", &(), GAS_LIMIT) + .expect("query authorization counter last authorizer") + .data; + assert_eq!(actual_authorizer, last_authorizer); + assert_nonce(session, contract, principal, nonce); +} + +fn assert_nonce( + session: &mut Session, + contract: ContractId, + principal: Principal, + nonce: u64, +) { + assert_contract_nonce( + session, + contract, + principal, + SET_VALUE_DOMAIN, + nonce, + ); +} + +fn assert_contract_nonce( + session: &mut Session, + contract: ContractId, + principal: Principal, + domain: NonceDomain, + nonce: u64, +) { + let actual_nonce: u64 = session + .call( + contract, + "nonce", + &NonceQuery { principal, domain }, + GAS_LIMIT, + ) + .expect("query contract nonce") + .data; + assert_eq!(actual_nonce, nonce); +} + +fn assert_drc20_balance( + session: &mut Session, + contract: ContractId, + account: Principal, + expected: u64, +) { + let actual: u64 = session + .call(contract, "balance_of", &BalanceOf20 { account }, GAS_LIMIT) + .expect("query drc20 balance") + .data; + assert_eq!(actual, expected); +} + +fn assert_proxy_value( + session: &mut Session, + contract: ContractId, + expected: u64, +) { + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value") + .data; + assert_eq!(value, expected); +} + +fn proxy_set_value_target( + proxy: ContractId, + value: u64, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(proxy, "set_value") + .with_args(&ProxySetValue { + value, + authorization: None, + }) + .expect("serialize proxy set_value args"), + salt, + } +} + +fn amount_hash(amount: u64) -> [u8; 32] { + host_queries::keccak256(Vec::from(amount.to_be_bytes())) +} + +fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + host_queries::keccak256(bytes) +} + +fn royalty_payload_hash(token_id: Option, info: RoyaltyInfo) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.royalty"[..]); + match token_id { + Some(token_id) => { + bytes.push(1); + bytes.extend_from_slice(&token_id.to_be_bytes()); + } + None => bytes.push(0), + } + push_principal(&mut bytes, info.receiver); + bytes.extend_from_slice(&info.basis_points.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + host_queries::keccak256(Vec::from(tag)) +} + +fn approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn proxy_value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} + +fn phoenix_principal(seed: u64) -> Principal { + let secret = SchnorrSecretKey::from(JubJubScalar::from(seed)); + let public = SchnorrPublicKey::from(&secret); + Principal::phoenix_public_key(&public) +} + +fn deploy( + session: &mut Session, + wasm_name: &str, + contract_id: [u8; 32], + init: &A, +) -> ContractId +where + A: for<'a> Serialize>, +{ + let id = ContractId::from_bytes(contract_id); + let wasm = fs::read(wasm_path(wasm_name)) + .unwrap_or_else(|err| panic!("read {wasm_name}: {err}")); + session + .deploy( + &wasm, + ContractData::builder() + .owner(OWNER_BYTES) + .contract_id(id) + .init_arg(init), + GAS_LIMIT, + ) + .unwrap_or_else(|err| panic!("deploy {wasm_name}: {err}")) +} + +fn wasm_path(name: &str) -> PathBuf { + workspace_root() + .join("target") + .join("wasm32-unknown-unknown") + .join("release") + .join(name) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("workspace root") + .to_path_buf() +} diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs new file mode 100644 index 00000000..94ca4a70 --- /dev/null +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -0,0 +1,2018 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use dusk_contract_standards::access::{ + AccessControl, Ownable, Ownable2Step, OwnerSet, Pausable, + DEFAULT_ADMIN_ROLE, +}; +use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, AuthorizedAction, + MoonlightAuthorization, PhoenixSignatureAuthorization, SignedAuthorization, +}; +use dusk_contract_standards::core::{ + CallContext, NonceEntry, NonceManager, Principal, PrincipalKind, + ReplayGuard, +}; +use dusk_contract_standards::governance::{ + MultisigConfig, MultisigController, MultisigControllerConfig, + MultisigControllerStatus, MultisigTarget, ThresholdMultisig, Timelock, + TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, + TIMELOCK_ADMIN_ROLE, +}; +use dusk_contract_standards::proxy::{ + StateStore, UpgradeActivated, UpgradeAdmin, UpgradeCancelled, + UpgradePrepared, UpgradeRolledBack, +}; +use dusk_contract_standards::security::ReentrancyGuard; +use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, + Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, SupplyCap, + TransferCall as Transfer20, TransferFromCall, VotingUnits, +}; +use dusk_contract_standards::token::drc721::{ + ApproveCall as Approve721, BalanceOf as BalanceOf721, Drc721, + Init as Init721, InitToken, IsApprovedForAll, OwnerOf, RoyaltyInfo, + RoyaltyRegistry, SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, + TokensOf, TransferFromCall as Transfer721, +}; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::transfer::data::ContractCall; +use dusk_core::JubJubScalar; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const TEST_CHAIN_ID: u8 = 0xD5; + +fn p(byte: u8) -> Principal { + Principal::phoenix([byte; 32]) +} + +fn c(byte: u8) -> ContractId { + ContractId::from_bytes([byte; 32]) +} + +fn multisig_target( + contract: u8, + function: &str, + args: impl AsRef<[u8]>, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(c(contract), function) + .with_raw_args(args.as_ref().to_vec()), + salt, + } +} + +fn assert_panics(f: impl FnOnce() -> R) { + assert!(catch_unwind(AssertUnwindSafe(f)).is_err()); +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +#[allow(clippy::too_many_arguments)] +fn signed_moonlight_action( + secret: BlsSecretKey, + public_key: BlsPublicKey, + principal: Principal, + contract: ContractId, + domain: [u8; 32], + action_id: [u8; 32], + payload_hash: [u8; 32], + nonce: u64, +) -> SignedAuthorization { + let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce, + expires_at: 0, + principal, + payload_hash, + }; + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key, + signature: secret.sign(&action.message_bytes()), + }) +} + +#[allow(clippy::too_many_arguments)] +fn signed_phoenix_action( + secret: SchnorrSecretKey, + public_key: SchnorrPublicKey, + principal: Principal, + contract: ContractId, + domain: [u8; 32], + action_id: [u8; 32], + payload_hash: [u8; 32], + nonce: u64, + rng_seed: u64, +) -> SignedAuthorization { + let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce, + expires_at: 0, + principal, + payload_hash, + }; + let mut rng = StdRng::seed_from_u64(rng_seed); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key, + signature: secret.sign(&mut rng, action.message_hash()), + replay_key: None, + }) +} + +#[test] +fn ownable_positive_and_negative_paths() { + let owner = p(1); + let next = p(2); + let stranger = p(3); + + let mut ownable = Ownable::new(); + ownable.init(owner); + assert_eq!(ownable.owner(), Some(owner)); + ownable.assert_owner(owner); + assert_panics(|| ownable.assert_owner(stranger)); + + ownable.transfer_ownership(owner, next); + assert_eq!(ownable.owner(), Some(next)); + assert_panics(|| ownable.transfer_ownership(owner, stranger)); + ownable.renounce_ownership(next); + assert_eq!(ownable.owner(), None); +} + +#[test] +fn ownable_two_step_requires_pending_owner_acceptance() { + let owner = p(1); + let next = p(2); + let stranger = p(3); + + let mut ownable = Ownable2Step::new(); + ownable.init(owner); + ownable.transfer_ownership(owner, next); + assert_eq!(ownable.owner(), Some(owner)); + assert_eq!(ownable.pending_owner(), Some(next)); + + assert_panics(|| ownable.accept_ownership(stranger)); + ownable.accept_ownership(next); + assert_eq!(ownable.owner(), Some(next)); + assert_eq!(ownable.pending_owner(), None); +} + +#[test] +fn access_control_admin_role_gates_grants_and_revokes() { + let admin = p(1); + let minter = p(2); + let stranger = p(3); + let minter_role = [7u8; 32]; + + let mut access = AccessControl::new(); + access.init_admin(admin); + assert!(access.has_role(DEFAULT_ADMIN_ROLE, admin)); + assert_panics(|| access.grant_role(stranger, minter_role, minter)); + + access.grant_role(admin, minter_role, minter); + assert!(access.has_role(minter_role, minter)); + access.assert_role(minter_role, minter); + + assert_panics(|| access.revoke_role(stranger, minter_role, minter)); + access.revoke_role(admin, minter_role, minter); + assert!(!access.has_role(minter_role, minter)); + + access.renounce_role(DEFAULT_ADMIN_ROLE, admin); + assert!(!access.has_role(DEFAULT_ADMIN_ROLE, admin)); + assert_panics(|| access.init_admin(stranger)); +} + +#[test] +fn owner_set_supports_mixed_dusk_principals() { + let moonlight = Principal::Moonlight([1u8; 193]); + let phoenix = p(2); + let contract = Principal::contract(c(3)); + let stranger = p(4); + + let mut owners = OwnerSet::new(); + owners.init([moonlight, phoenix]); + assert!(owners.is_owner(moonlight)); + assert!(owners.is_owner(phoenix)); + assert_eq!(owners.count_kind(PrincipalKind::Moonlight), 1); + assert_eq!(owners.count_kind(PrincipalKind::Phoenix), 1); + assert_panics(|| owners.assert_owner(stranger)); + + owners.add_owner(moonlight, contract); + assert_eq!(owners.count_kind(PrincipalKind::Contract), 1); + assert_panics(|| owners.add_owner(stranger, p(5))); + + owners.remove_owner(phoenix, moonlight); + assert!(!owners.is_owner(moonlight)); + owners.replace_owner(contract, phoenix, moonlight); + assert!(owners.is_owner(moonlight)); + assert!(!owners.is_owner(phoenix)); + + let before = owners.owners(); + assert_panics(|| { + owners.replace_owner(contract, moonlight, Principal::contract(c(0))); + }); + assert_eq!(owners.owners(), before); + assert_panics(|| { + owners.replace_owner(contract, moonlight, contract); + }); + assert_eq!(owners.owners(), before); + assert_panics(|| { + owners.replace_owner(contract, p(99), p(10)); + }); + assert_eq!(owners.owners(), before); +} + +#[test] +fn threshold_multisig_requires_distinct_quorum_before_nonce_consumption() { + let owner_a_sk = moonlight_secret(101); + let owner_a_pk = BlsPublicKey::from(&owner_a_sk); + let owner_a = Principal::moonlight(&owner_a_pk); + let owner_b_sk = moonlight_secret(102); + let owner_b_pk = BlsPublicKey::from(&owner_b_sk); + let owner_b = Principal::moonlight(&owner_b_pk); + let outsider_sk = moonlight_secret(103); + let outsider_pk = BlsPublicKey::from(&outsider_sk); + let outsider = Principal::moonlight(&outsider_pk); + + let contract = c(104); + let domain = [105u8; 32]; + let action_id = [106u8; 32]; + let payload_hash = [107u8; 32]; + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + }); + + let approval_a = signed_moonlight_action( + owner_a_sk, + owner_a_pk, + owner_a, + contract, + domain, + action_id, + payload_hash, + 0, + ); + let approval_b = signed_moonlight_action( + owner_b_sk, + owner_b_pk, + owner_b, + contract, + domain, + action_id, + payload_hash, + 0, + ); + let outsider_approval = signed_moonlight_action( + outsider_sk, + outsider_pk, + outsider, + contract, + domain, + action_id, + payload_hash, + 0, + ); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + std::slice::from_ref(&approval_a), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_a.clone()], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), outsider_approval], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + assert_eq!(manager.nonce(outsider, domain), 0); + + let wrong_envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [108u8; 32], + ); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_b.clone()], + wrong_envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + assert_eq!(manager.nonce(owner_b, domain), 0); + + let signers = multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_b.clone()], + envelope, + 0, + ); + assert_eq!(signers.len(), 2); + assert!(signers.contains(&owner_a)); + assert!(signers.contains(&owner_b)); + assert_eq!(manager.nonce(owner_a, domain), 1); + assert_eq!(manager.nonce(owner_b, domain), 1); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a, approval_b], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 1); + assert_eq!(manager.nonce(owner_b, domain), 1); +} + +#[test] +fn threshold_multisig_counts_observed_moonlight_and_contract_callers() { + let contract_owner = Principal::contract(c(110)); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(111u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix_owner = Principal::phoenix_public_key(&phoenix_pk); + let contract = c(112); + let domain = [113u8; 32]; + let action_id = [114u8; 32]; + let payload_hash = [115u8; 32]; + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![contract_owner, phoenix_owner], + threshold: 2, + }); + + let phoenix_approval = signed_phoenix_action( + phoenix_sk, + phoenix_pk, + phoenix_owner, + contract, + domain, + action_id, + payload_hash, + 0, + 116, + ); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::from_principal(phoenix_owner), + &[], + envelope, + 0, + ); + }); + + let signers = multisig.authorize_action( + &mut manager, + CallContext::from_principal(contract_owner), + &[phoenix_approval], + envelope, + 0, + ); + assert_eq!(signers, vec![phoenix_owner, contract_owner]); + assert_eq!(manager.nonce(phoenix_owner, domain), 1); +} + +#[test] +fn threshold_multisig_owner_management_requires_quorum_and_is_atomic() { + let owner_a = p(120); + let owner_b = p(121); + let owner_c = p(122); + let owner_d = p(123); + let outsider = p(124); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + }); + + assert_panics(|| multisig.set_threshold(&[owner_a], 1)); + assert_eq!(multisig.threshold(), 2); + + assert_panics(|| multisig.add_owner(&[owner_a, outsider], owner_d)); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c]); + + multisig.add_owner(&[owner_a, owner_b], owner_d); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c, owner_d]); + + let before = multisig.owners(); + assert_panics(|| { + multisig.replace_owner(&[owner_a, owner_b], owner_c, owner_d) + }); + assert_eq!(multisig.owners(), before); + + assert_panics(|| multisig.remove_owner(&[owner_a, owner_b], owner_d, 4)); + assert_eq!(multisig.owners(), before); + assert_eq!(multisig.threshold(), 2); + + multisig.remove_owner(&[owner_a, owner_b], owner_d, 2); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c]); + + multisig.replace_owner(&[owner_a, owner_b], owner_c, owner_d); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_d]); + + multisig.set_threshold(&[owner_a, owner_b], 3); + assert_eq!(multisig.threshold(), 3); + assert_panics(|| multisig.set_threshold(&[owner_a, owner_b], 0)); + assert_eq!(multisig.threshold(), 3); +} + +#[test] +fn multisig_controller_proposes_confirms_executes_and_tombstones() { + let owner_a = p(130); + let owner_b = p(131); + let owner_c = p(132); + let outsider = p(133); + let id = [134u8; 32]; + let target = multisig_target(135, "set_value", [1, 2, 3], [136u8; 32]); + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 5, + }); + + assert_panics(|| controller.confirm(id, owner_a, 0)); + assert_panics(|| controller.propose(id, target.clone(), outsider, 0)); + assert!(controller.proposal(id).is_none()); + + let proposed = controller.propose(id, target.clone(), owner_a, 0); + assert_eq!(proposed.status, MultisigControllerStatus::Proposed); + assert_eq!(proposed.confirmations, 1); + assert!(proposed.ready_operation.is_none()); + assert_eq!( + controller.proposal(id).expect("pending").confirmations, + vec![owner_a] + ); + + assert_panics(|| controller.confirm(id, owner_a, 1)); + assert_eq!( + controller + .proposal(id) + .expect("still pending") + .confirmations, + vec![owner_a] + ); + + let ready = controller.confirm(id, owner_b, 1); + assert_eq!(ready.status, MultisigControllerStatus::Ready); + assert_eq!(ready.confirmations, 2); + let operation = ready.ready_operation.expect("ready operation"); + assert_eq!(operation.target, target); + assert_eq!(operation.confirmations, vec![owner_a, owner_b]); + assert!(controller.proposal(id).is_none()); + assert_eq!(controller.tombstone_expiry(id), Some(6)); + + assert_panics(|| { + controller.propose( + id, + multisig_target(135, "set_value", [4], [136u8; 32]), + owner_c, + 2, + ) + }); + assert_eq!(controller.tombstone_expiry(id), Some(6)); + + let reproposed = controller.propose( + id, + multisig_target(135, "set_value", [4], [136u8; 32]), + owner_c, + 7, + ); + assert_eq!(reproposed.status, MultisigControllerStatus::Proposed); + assert_eq!(controller.tombstone_expiry(id), None); +} + +#[test] +fn multisig_controller_expiry_cancel_and_authority_updates_are_atomic() { + let owner_a = p(140); + let owner_b = p(141); + let owner_c = p(142); + let owner_d = p(143); + let outsider = p(144); + let id_a = [145u8; 32]; + let id_b = [146u8; 32]; + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + proposal_ttl: 3, + tombstone_ttl: 4, + }); + assert_panics(|| { + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 1, + proposal_ttl: 1, + tombstone_ttl: 1, + }); + }); + + controller.propose( + id_a, + multisig_target(147, "one", [], [148u8; 32]), + owner_a, + 10, + ); + assert!(controller.proposal(id_a).is_some()); + assert_panics(|| controller.confirm(id_a, owner_b, 14)); + assert!(controller.proposal(id_a).is_none()); + controller.propose( + id_b, + multisig_target(149, "two", [], [150u8; 32]), + owner_a, + 14, + ); + assert!(controller.proposal(id_a).is_none()); + assert!(controller.proposal(id_b).is_some()); + + assert_panics(|| controller.cancel(id_b, &[owner_a], 14)); + assert!(controller.proposal(id_b).is_some()); + let cancelled = controller.cancel(id_b, &[owner_a, owner_b], 14); + assert_eq!(cancelled.signers, vec![owner_a, owner_b]); + assert!(controller.proposal(id_b).is_none()); + + controller.propose( + id_a, + multisig_target(151, "three", [], [152u8; 32]), + owner_a, + 15, + ); + let before = controller.owners(); + assert_panics(|| { + controller.update_authority( + &[owner_a, outsider], + vec![owner_a, owner_d], + 2, + ); + }); + assert_eq!(controller.owners(), before); + assert!(controller.proposal(id_a).is_some()); + + let event = controller.update_authority( + &[owner_a, owner_b], + vec![owner_a, owner_b, owner_c, owner_d], + 2, + ); + assert_eq!(event.previous_owners, before); + assert_eq!(event.owners, vec![owner_a, owner_b, owner_c, owner_d]); + assert!(event.removed_operations.is_empty()); + assert!(controller.proposal(id_a).is_some()); + + let event = controller.update_authority( + &[owner_a, owner_b], + vec![owner_a, owner_d], + 2, + ); + assert_eq!(event.removed_operations, vec![id_a]); + assert!(controller.proposal(id_a).is_none()); + + let before_ttl = controller.proposal_ttl(); + assert_panics(|| controller.set_time_limits(&[owner_a], 0, 4)); + assert_eq!(controller.proposal_ttl(), before_ttl); + let event = controller.set_time_limits(&[owner_a, owner_d], 8, 9); + assert_eq!(event.previous_proposal_ttl, 3); + assert_eq!(event.proposal_ttl, 8); + assert_eq!(controller.tombstone_ttl(), 9); +} + +#[test] +fn multisig_controller_rejects_bad_targets_and_config() { + let owner_a = p(153); + let owner_b = p(154); + let mut controller = MultisigController::new(); + + assert_panics(|| { + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + proposal_ttl: 0, + tombstone_ttl: 1, + }); + }); + assert!(!controller.is_initialized()); + + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + proposal_ttl: 1, + tombstone_ttl: 1, + }); + + assert_panics(|| { + controller.propose( + [155u8; 32], + multisig_target(0, "call", [], [156u8; 32]), + owner_a, + 0, + ); + }); + assert_panics(|| { + controller.propose( + [157u8; 32], + multisig_target(158, "", [], [159u8; 32]), + owner_a, + 0, + ); + }); +} + +#[test] +fn replay_guard_rejects_reuse_across_same_principal() { + let owner = p(1); + let other = p(2); + let key = [9u8; 32]; + + let mut replay = ReplayGuard::new(); + assert!(!replay.is_used(owner, key)); + replay.consume(owner, key); + assert!(replay.is_used(owner, key)); + replay.consume(other, key); + assert_panics(|| replay.consume(owner, key)); +} + +#[test] +fn nonce_manager_keeps_independent_principal_and_domain_streams() { + let moonlight = Principal::Moonlight([1u8; 193]); + let phoenix = p(2); + let permit_domain = [10u8; 32]; + let vote_domain = [11u8; 32]; + + let mut nonces = NonceManager::new(); + assert_eq!(nonces.current(moonlight, permit_domain), 0); + assert_eq!(nonces.use_next(moonlight, permit_domain), 0); + assert_eq!(nonces.current(moonlight, permit_domain), 1); + assert_eq!(nonces.current(phoenix, permit_domain), 0); + assert_eq!(nonces.current(moonlight, vote_domain), 0); + + assert_panics(|| { + nonces.consume(moonlight, permit_domain, 0); + }); + nonces.invalidate_until(moonlight, permit_domain, 9); + assert_eq!(nonces.current(moonlight, permit_domain), 9); + assert_panics(|| { + nonces.invalidate_until(moonlight, permit_domain, 8); + }); + + nonces.import_entries([NonceEntry { + principal: moonlight, + domain: permit_domain, + nonce: 12, + }]); + assert_eq!(nonces.current(moonlight, permit_domain), 12); +} + +#[test] +fn authorization_manager_verifies_moonlight_and_phoenix_paths() { + let sk = moonlight_secret(7); + let pk = BlsPublicKey::from(&sk); + let moonlight = Principal::moonlight(&pk); + let contract = c(33); + let domain = [44u8; 32]; + let action_id = [55u8; 32]; + + let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: moonlight, + payload_hash: [66u8; 32], + }; + let signature = sk.sign(&action.message_bytes()); + let auth = MoonlightAuthorization { + action, + public_key: pk, + signature, + }; + + let mut authorizations = AuthorizationManager::new(); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [66u8; 32], + ); + assert_eq!( + authorizations.authorize_moonlight_action(&auth, envelope, 99), + moonlight + ); + assert_eq!(authorizations.nonce(moonlight, domain), 1); + assert_panics(|| { + authorizations.authorize_moonlight_action(&auth, envelope, 99); + }); + + let bad_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + principal: p(99), + ..action + }; + let bad_signature = sk.sign(&bad_action.message_bytes()); + let bad_auth = MoonlightAuthorization { + action: bad_action, + public_key: pk, + signature: bad_signature, + }; + assert_panics(|| { + authorizations.authorize_moonlight_action(&bad_auth, envelope, 99); + }); + + let mut rng = StdRng::seed_from_u64(1234); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain: [77u8; 32], + action_id, + nonce: 0, + expires_at: 0, + principal: phoenix, + payload_hash: [22u8; 32], + }; + let phoenix_auth = PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: Some([99u8; 32]), + }; + let phoenix_envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [22u8; 32], + ); + assert_eq!( + authorizations.authorize_phoenix_action( + &phoenix_auth, + phoenix_envelope, + 1, + ), + phoenix + ); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + assert!(authorizations.replay_used(phoenix, [99u8; 32])); + + let expired_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + expires_at: 9, + nonce: 1, + ..phoenix_action + }; + let expired = PhoenixSignatureAuthorization { + action: expired_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, expired_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix_action(&expired, phoenix_envelope, 10); + }); + + let bad_signature_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + payload_hash: [23u8; 32], + ..phoenix_action + }; + let bad_signature = PhoenixSignatureAuthorization { + action: bad_signature_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix_action( + &bad_signature, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [23u8; 32], + ), + 1, + ); + }); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + + let replay_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + payload_hash: [24u8; 32], + ..phoenix_action + }; + let replay = PhoenixSignatureAuthorization { + action: replay_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, replay_action.message_hash()), + replay_key: Some([99u8; 32]), + }; + assert_panics(|| { + authorizations.authorize_phoenix_action( + &replay, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [24u8; 32], + ), + 1, + ); + }); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + + let wrong_sk = SchnorrSecretKey::from(JubJubScalar::from(89u64)); + let wrong_pk = SchnorrPublicKey::from(&wrong_sk); + let wrong_key = PhoenixSignatureAuthorization { + action: AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + ..phoenix_action + }, + public_key: wrong_pk, + signature: wrong_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix_action( + &wrong_key, + phoenix_envelope, + 1, + ); + }); + + let bounded_domain = [88u8; 32]; + let bounded_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain: bounded_domain, + action_id, + nonce: 0, + expires_at: 10, + principal: moonlight, + payload_hash: [1u8; 32], + }; + let bounded_signed = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: bounded_action, + public_key: pk, + signature: sk.sign(&bounded_action.message_bytes()), + }); + let mut bounded = AuthorizationManager::new(); + assert_panics(|| { + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [2u8; 32], + ), + 9, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 0); + assert_panics(|| { + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new( + TEST_CHAIN_ID.wrapping_add(1), + contract, + bounded_domain, + action_id, + [1u8; 32], + ), + 9, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 0); + assert_eq!( + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [1u8; 32] + ), + 10, + ), + moonlight + ); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 1); + + let expired_bounded_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + expires_at: 10, + payload_hash: [3u8; 32], + ..bounded_action + }; + let expired_bounded = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: expired_bounded_action, + public_key: pk, + signature: sk.sign(&expired_bounded_action.message_bytes()), + }); + assert_panics(|| { + bounded.authorize_signed_action( + &expired_bounded, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [3u8; 32], + ), + 11, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 1); +} + +#[test] +fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { + let moonlight_sk = moonlight_secret(17); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight = Principal::moonlight(&moonlight_pk); + let contract_principal = Principal::contract(c(44)); + let contract = c(45); + let domain = [46u8; 32]; + let action_id = [47u8; 32]; + + let mut manager = AuthorizationManager::new(); + assert_eq!( + manager.authorize_principal( + moonlight, + CallContext::from_principal(moonlight), + None, + 0, + ), + moonlight + ); + assert_eq!( + manager.authorize_principal( + contract_principal, + CallContext::from_principal(contract_principal), + None, + 0, + ), + contract_principal + ); + + let moonlight_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: moonlight, + payload_hash: [48u8; 32], + }; + let moonlight_signed = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: moonlight_action, + public_key: moonlight_pk, + signature: moonlight_sk.sign(&moonlight_action.message_bytes()), + }); + assert_eq!( + manager.authorize_principal( + moonlight, + CallContext::none(), + Some(&moonlight_signed), + 0, + ), + moonlight + ); + + let mut rng = StdRng::seed_from_u64(9876); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(18u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: phoenix, + payload_hash: [49u8; 32], + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + + let mut ownable = Ownable::new(); + ownable.init(phoenix); + assert_eq!( + ownable.authorize_owner_action( + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [49u8; 32] + ), + 0, + ), + phoenix + ); + + let mut access = AccessControl::new(); + let role = [50u8; 32]; + access.init_admin(moonlight); + access.grant_role(moonlight, role, phoenix); + let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 1, + payload_hash: [51u8; 32], + ..phoenix_action + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + assert_eq!( + access.authorize_role_action( + role, + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [51u8; 32] + ), + 0, + ), + phoenix + ); + + let upgrade = UpgradeAdmin::new(phoenix, c(52), 0, 0); + let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + nonce: 2, + payload_hash: [53u8; 32], + ..phoenix_action + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + assert_eq!( + upgrade.authorize_admin_action( + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [53u8; 32] + ), + 0, + ), + phoenix + ); + assert_panics(|| { + manager.authorize_principal( + phoenix, + CallContext::from_principal(phoenix), + None, + 0, + ); + }); +} + +#[test] +fn failed_owner_role_and_admin_signed_checks_do_not_consume_nonce() { + let signer_sk = moonlight_secret(70); + let signer_pk = BlsPublicKey::from(&signer_sk); + let signer = Principal::moonlight(&signer_pk); + let owner = p(71); + let admin = p(72); + let contract = c(73); + let implementation = c(74); + let role = [75u8; 32]; + let domain = [76u8; 32]; + let action_id = [77u8; 32]; + let payload_hash = [78u8; 32]; + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); + let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: signer, + payload_hash, + }; + let signed = SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: signer_pk, + signature: signer_sk.sign(&action.message_bytes()), + }); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + manager.authorize_principal_action( + owner, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); + + let mut access = AccessControl::new(); + access.init_admin(admin); + access.grant_role(admin, role, owner); + assert_panics(|| { + access.authorize_role_action( + role, + &mut manager, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); + + let upgrades = UpgradeAdmin::new(admin, implementation, 0, 0); + assert_panics(|| { + upgrades.authorize_admin_action( + &mut manager, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); +} + +#[test] +fn reentrancy_guard_blocks_nested_entry() { + let mut guard = ReentrancyGuard::new(); + guard.enter(); + assert_panics(|| guard.enter()); + guard.exit(); + guard.enter(); + guard.exit(); +} + +#[test] +fn reentrancy_guard_run_resets_after_panic() { + let mut guard = ReentrancyGuard::new(); + assert_panics(|| { + guard.run(|| panic!("boom")); + }); + assert!(!guard.entered()); + guard.enter(); + guard.exit(); +} + +#[test] +fn timelock_schedules_executes_and_rejects_invalid_states() { + let op = [8u8; 32]; + let mut timelock = Timelock::new(5); + + let ready_at = timelock.schedule(op, 10, vec![1, 2, 3]); + assert_eq!(ready_at, 15); + assert!(!timelock.is_ready(op, 14)); + assert_panics(|| { + timelock.execute(op, 14); + }); + assert_eq!(timelock.execute(op, 15), vec![1, 2, 3]); + assert_panics(|| { + timelock.execute(op, 16); + }); + + let other = [9u8; 32]; + timelock.schedule(other, 20, vec![]); + timelock.cancel(other); + assert!(timelock.get(other).is_none()); +} + +#[test] +fn timelock_controller_gates_schedule_execute_cancel_and_policy() { + let admin = p(1); + let proposer = p(2); + let executor = p(3); + let canceller = p(4); + let stranger = p(5); + let controller_principal = p(6); + let op = [10u8; 32]; + + let mut controller = + TimelockController::new(controller_principal, admin, 5); + assert_eq!(controller.self_principal(), controller_principal); + assert!(controller.has_role(TIMELOCK_ADMIN_ROLE, admin)); + controller.grant_role(admin, PROPOSER_ROLE, proposer); + controller.grant_role(admin, EXECUTOR_ROLE, executor); + controller.grant_role(admin, CANCELLER_ROLE, canceller); + + assert_panics(|| { + controller.schedule(stranger, op, 10, vec![]); + }); + assert_eq!(controller.schedule(proposer, op, 10, vec![1]), 15); + assert_panics(|| { + controller.execute(executor, op, 14); + }); + assert_eq!(controller.execute(executor, op, 15), vec![1]); + assert_panics(|| { + controller.execute(executor, op, 16); + }); + + let other = [11u8; 32]; + controller.schedule(admin, other, 20, vec![2]); + assert_panics(|| { + controller.cancel(stranger, other); + }); + controller.cancel(canceller, other); + assert!(controller.get(other).is_none()); + + assert_panics(|| controller.set_min_delay(stranger, 9)); + assert_panics(|| controller.set_min_delay(admin, 9)); + let delay_change = [12u8; 32]; + assert_eq!( + controller.schedule_min_delay_change(proposer, delay_change, 20, 9), + 25 + ); + assert_panics(|| { + controller.execute_min_delay_change(executor, delay_change, 24); + }); + assert_eq!( + controller.execute_min_delay_change(executor, delay_change, 25), + 9 + ); + assert_eq!(controller.timelock().min_delay(), 9); +} + +#[test] +fn pausable_tracks_state_and_assertions() { + let mut pausable = Pausable::new(); + pausable.assert_not_paused(); + assert_panics(|| pausable.assert_paused()); + pausable.pause(); + pausable.assert_paused(); + assert_panics(|| pausable.assert_not_paused()); + pausable.unpause(); + pausable.assert_not_paused(); +} + +#[test] +fn drc20_rejects_use_before_init_and_double_init() { + let owner = p(1); + let mut token = Drc20::new(); + + assert_panics(|| { + token.balance_of(BalanceOf20 { account: owner }); + }); + assert_panics(|| { + token.mint(owner, 1); + }); + + token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![], + }); + assert_eq!(token.total_supply(), 0); + assert_panics(|| { + token.init(Init20 { + name: "Again".into(), + symbol: "AGAIN".into(), + decimals: 9, + initial_balances: vec![], + }); + }); + + let mut bad = Drc20::new(); + assert_panics(|| { + bad.init(Init20 { + name: "Bad".into(), + symbol: "BAD".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: p(0), + amount: 1, + }], + }); + }); + assert_panics(|| { + bad.total_supply(); + }); + bad.init(Init20 { + name: "Recovered".into(), + symbol: "GOOD".into(), + decimals: 9, + initial_balances: vec![], + }); + assert_eq!(bad.total_supply(), 0); + + let mut overflowing = Drc20::new(); + assert_panics(|| { + overflowing.init(Init20 { + name: "Overflow".into(), + symbol: "OVR".into(), + decimals: 9, + initial_balances: vec![ + InitBalance { + account: owner, + amount: u64::MAX, + }, + InitBalance { + account: p(2), + amount: 1, + }, + ], + }); + }); + assert_panics(|| { + overflowing.total_supply(); + }); +} + +#[test] +fn drc20_supports_transfer_allowance_mint_and_burn() { + let owner = p(1); + let receiver = p(2); + let spender = p(3); + + let mut token = Drc20::new(); + let mint_events = token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: 100, + }], + }); + assert_eq!(mint_events.len(), 1); + assert_eq!(token.total_supply(), 100); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 100); + + let transfer = token.transfer( + owner, + Transfer20 { + to: receiver, + amount: 40, + }, + ); + assert_eq!(transfer.from, owner); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 60); + assert_eq!(token.balance_of(BalanceOf20 { account: receiver }), 40); + + let approval = token.approve( + receiver, + ApproveCall { + spender, + amount: 15, + }, + ); + assert_eq!(approval.owner, receiver); + assert_eq!( + token.allowance(Allowance { + owner: receiver, + spender, + }), + 15 + ); + let increased = token.increase_allowance( + receiver, + IncreaseAllowanceCall { + spender, + added_amount: 5, + }, + ); + assert_eq!(increased.amount, 20); + let decreased = token.decrease_allowance( + receiver, + DecreaseAllowanceCall { + spender, + subtracted_amount: 5, + }, + ); + assert_eq!(decreased.amount, 15); + + token.transfer_from( + spender, + TransferFromCall { + owner: receiver, + to: owner, + amount: 10, + }, + ); + assert_eq!( + token.allowance(Allowance { + owner: receiver, + spender, + }), + 5 + ); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 70); + + token.mint(receiver, 5); + assert_eq!(token.total_supply(), 105); + token.burn(receiver, 5); + assert_eq!(token.total_supply(), 100); + + assert_panics(|| { + token.transfer( + receiver, + Transfer20 { + to: owner, + amount: 1_000, + }, + ); + }); + assert_panics(|| { + token.decrease_allowance( + receiver, + DecreaseAllowanceCall { + spender, + subtracted_amount: 100, + }, + ); + }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner: receiver, + to: owner, + amount: 10, + }, + ); + }); +} + +#[test] +fn drc20_failed_operations_do_not_leave_partial_state() { + let owner = p(1); + let receiver = p(2); + let spender = p(3); + let zero = p(0); + + let mut token = Drc20::new(); + token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: 10, + }], + }); + token.approve(owner, ApproveCall { spender, amount: 7 }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner, + to: zero, + amount: 3, + }, + ); + }); + assert_eq!( + token.allowance(Allowance { owner, spender }), + 7, + "zero-recipient transfer_from must not spend allowance" + ); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 10); + + let empty_owner = p(4); + token.approve_for(empty_owner, ApproveCall { spender, amount: 7 }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner: empty_owner, + to: receiver, + amount: 3, + }, + ); + }); + assert_eq!( + token.allowance(Allowance { + owner: empty_owner, + spender, + }), + 7, + "failed transfer_from must not spend allowance before balance checks" + ); + assert_eq!(token.balance_of(BalanceOf20 { account: receiver }), 0); + + let mut max_supply = Drc20::new(); + max_supply.init(Init20 { + name: "Max".into(), + symbol: "MAX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: u64::MAX, + }], + }); + max_supply.transfer( + owner, + Transfer20 { + to: owner, + amount: 1, + }, + ); + assert_eq!( + max_supply.balance_of(BalanceOf20 { account: owner }), + u64::MAX + ); + assert_panics(|| { + max_supply.mint(receiver, 1); + }); + assert_eq!(max_supply.total_supply(), u64::MAX); + assert_eq!(max_supply.balance_of(BalanceOf20 { account: receiver }), 0); +} + +#[test] +fn drc20_supply_cap_and_voting_units_cover_policy_hooks() { + let owner = p(1); + let receiver = p(2); + + let mut cap = SupplyCap::new(100); + cap.assert_mint(90, 10); + assert_eq!(cap.remaining(40), 60); + assert_panics(|| cap.assert_mint(90, 11)); + assert_panics(|| cap.set_cap(80, 79)); + cap.set_cap(80, 120); + assert_eq!(cap.cap(), 120); + + let mut votes = VotingUnits::new(); + votes.move_units(None, Some(owner), 100, 10); + assert_eq!(votes.latest_votes(owner), 100); + assert_eq!(votes.latest_total_supply(), 100); + assert_eq!(votes.past_votes(owner, 9), 0); + assert_eq!(votes.past_votes(owner, 10), 100); + + votes.move_units(Some(owner), Some(receiver), 40, 11); + assert_eq!(votes.latest_votes(owner), 60); + assert_eq!(votes.latest_votes(receiver), 40); + assert_eq!(votes.latest_total_supply(), 100); + assert_eq!(votes.past_votes(owner, 10), 100); + assert_eq!(votes.past_votes(owner, 11), 60); + + votes.move_units(Some(receiver), None, 5, 12); + assert_eq!(votes.latest_votes(receiver), 35); + assert_eq!(votes.latest_total_supply(), 95); + assert_eq!(votes.past_total_supply(11), 100); + assert_eq!(votes.past_total_supply(12), 95); + + assert_panics(|| votes.move_units(Some(receiver), None, 100, 13)); + assert_panics(|| votes.write_votes(owner, 9, 1)); +} + +#[test] +fn drc721_rejects_use_before_init_and_double_init() { + let owner = p(1); + let mut token = Drc721::new(); + + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); + assert_panics(|| { + token.mint(owner, 1); + }); + + token.init(Init721 { + name: "Dusk NFT".into(), + symbol: "DNFT".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + assert_eq!(token.total_supply(), 0); + assert_panics(|| { + token.init(Init721 { + name: "Again".into(), + symbol: "AGAIN".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + }); + + let mut bad = Drc721::new(); + assert_panics(|| { + bad.init(Init721 { + name: "Bad".into(), + symbol: "BAD".into(), + base_uri: String::new(), + initial_tokens: vec![InitToken { + account: p(0), + token_id: 1, + }], + }); + }); + assert_panics(|| { + bad.total_supply(); + }); + bad.init(Init721 { + name: "Recovered".into(), + symbol: "GOOD".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + assert_eq!(bad.total_supply(), 0); + + let mut duplicate = Drc721::new(); + assert_panics(|| { + duplicate.init(Init721 { + name: "Duplicate".into(), + symbol: "DUP".into(), + base_uri: String::new(), + initial_tokens: vec![ + InitToken { + account: owner, + token_id: 1, + }, + InitToken { + account: p(2), + token_id: 1, + }, + ], + }); + }); + assert_panics(|| { + duplicate.total_supply(); + }); +} + +#[test] +fn drc721_supports_approval_operator_transfer_and_burn() { + let owner = p(1); + let receiver = p(2); + let approved = p(3); + let operator = p(4); + + let mut token = Drc721::new(); + let events = token.init(Init721 { + name: "Dusk NFT".into(), + symbol: "DNFT".into(), + base_uri: "ipfs://collection/".into(), + initial_tokens: vec![InitToken { + account: owner, + token_id: 1, + }], + }); + assert_eq!(events.len(), 1); + assert_eq!(token.total_supply(), 1); + assert_eq!(token.owner_of(OwnerOf { token_id: 1 }), owner); + assert_eq!(token.balance_of(BalanceOf721 { account: owner }), 1); + assert_eq!(token.token_by_index(TokenByIndex { index: 0 }), 1); + assert_eq!( + token.token_of_owner_by_index(TokenOfOwnerByIndex { owner, index: 0 }), + 1 + ); + assert_eq!(token.tokens_of(TokensOf { owner }), vec![1]); + + token.approve( + owner, + Approve721 { + approved, + token_id: 1, + }, + ); + token.transfer_from( + approved, + Transfer721 { + from: owner, + to: receiver, + token_id: 1, + }, + ); + assert_eq!(token.owner_of(OwnerOf { token_id: 1 }), receiver); + assert_eq!(token.balance_of(BalanceOf721 { account: owner }), 0); + assert_eq!(token.balance_of(BalanceOf721 { account: receiver }), 1); + assert_eq!(token.tokens_of(TokensOf { owner }), Vec::::new()); + assert_eq!(token.tokens_of(TokensOf { owner: receiver }), vec![1]); + + token.set_approval_for_all( + receiver, + SetApprovalForAllCall { + operator, + approved: true, + }, + ); + assert!(token.is_approved_for_all(IsApprovedForAll { + owner: receiver, + operator, + })); + + token.burn(operator, 1); + assert_eq!(token.total_supply(), 0); + assert_eq!(token.balance_of(BalanceOf721 { account: receiver }), 0); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); + assert_eq!( + token.tokens_of(TokensOf { owner: receiver }), + Vec::::new() + ); + + token.mint(owner, 2); + token.burn(owner, 2); + assert_eq!(token.total_supply(), 0); + + assert_panics(|| { + token.transfer_from( + owner, + Transfer721 { + from: receiver, + to: owner, + token_id: 1, + }, + ); + }); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 2 }); + }); +} + +#[test] +fn drc721_royalty_registry_quotes_default_and_token_overrides() { + let default_receiver = p(7); + let token_receiver = p(8); + let mut royalties = RoyaltyRegistry::new(); + + let empty = royalties.royalty_info(1, 10_000); + assert_eq!( + empty.receiver, + dusk_contract_standards::token::drc721::ZERO_PRINCIPAL + ); + assert_eq!(empty.amount, 0); + + royalties.set_default_royalty(RoyaltyInfo { + receiver: default_receiver, + basis_points: 500, + }); + assert_eq!(royalties.royalty_info(1, 10_000).amount, 500); + assert_eq!(royalties.royalty_info(1, 10_000).receiver, default_receiver); + + royalties.set_token_royalty( + 1, + RoyaltyInfo { + receiver: token_receiver, + basis_points: 1_250, + }, + ); + let quote = royalties.royalty_info(1, 8_000); + assert_eq!(quote.receiver, token_receiver); + assert_eq!(quote.amount, 1_000); + + royalties.clear_token_royalty(1); + assert_eq!(royalties.royalty_info(1, 10_000).receiver, default_receiver); + royalties.clear_default_royalty(); + assert_eq!(royalties.royalty_info(1, 10_000).amount, 0); + + assert_panics(|| { + royalties.set_default_royalty(RoyaltyInfo { + receiver: dusk_contract_standards::token::drc721::ZERO_PRINCIPAL, + basis_points: 1, + }); + }); + assert_panics(|| { + royalties.set_default_royalty(RoyaltyInfo { + receiver: default_receiver, + basis_points: 10_001, + }); + }); +} + +#[test] +fn proxy_upgrade_admin_enforces_delay_and_rollback_window() { + let admin = p(1); + let stranger = p(2); + let implementation_a = c(10); + let implementation_b = c(11); + + assert_panics(|| { + UpgradeAdmin::new(p(0), implementation_a, 0, 0); + }); + assert_panics(|| { + UpgradeAdmin::new(admin, c(0), 0, 0); + }); + + let mut upgrades = UpgradeAdmin::new(admin, implementation_a, 5, 10); + assert_eq!(upgrades.implementation(), implementation_a); + assert_panics(|| { + upgrades.prepare(stranger, implementation_b, 0, vec![]); + }); + + assert_eq!( + upgrades.prepare(admin, implementation_b, 100, vec![1, 2, 3]), + UpgradePrepared { + implementation: implementation_b, + eta: 105, + } + ); + assert_panics(|| { + upgrades.activate(admin, 104); + }); + let (migration, activated) = upgrades.activate_with_event(admin, 105); + assert_eq!(migration, vec![1, 2, 3]); + assert_eq!( + activated, + UpgradeActivated { + previous_implementation: implementation_a, + implementation: implementation_b, + rollback_deadline: 115, + } + ); + assert_eq!(upgrades.implementation(), implementation_b); + assert_eq!(upgrades.rollback_deadline(), 115); + + assert_eq!( + upgrades.rollback_with_event(admin, 110), + UpgradeRolledBack { + from_implementation: implementation_b, + restored_implementation: implementation_a, + } + ); + assert_eq!(upgrades.implementation(), implementation_a); + + upgrades.prepare(admin, implementation_b, 200, vec![]); + assert_eq!( + upgrades.cancel_pending(admin), + UpgradeCancelled { + implementation: implementation_b, + } + ); + assert_panics(|| { + upgrades.activate(admin, 205); + }); + + upgrades.prepare(admin, implementation_b, 200, vec![]); + upgrades.activate(admin, 205); + assert_panics(|| upgrades.rollback(admin, 216)); + upgrades.finalize_rollback_window(admin, 216); + assert_eq!(upgrades.rollback_deadline(), 0); + + let mut delay_overflow = UpgradeAdmin::new(admin, implementation_a, 1, 0); + assert_panics(|| { + delay_overflow.prepare(admin, implementation_b, u64::MAX, vec![]); + }); + assert_eq!(delay_overflow.implementation(), implementation_a); + + let mut rollback_overflow = + UpgradeAdmin::new(admin, implementation_a, 0, 1); + rollback_overflow.prepare(admin, implementation_b, u64::MAX, vec![]); + assert_panics(|| { + rollback_overflow.activate(admin, u64::MAX); + }); + assert_eq!(rollback_overflow.implementation(), implementation_a); + assert!(rollback_overflow.pending().is_some()); +} + +#[test] +fn proxy_state_store_keeps_plain_and_namespaced_state_separate() { + let namespace_a = [1u8; 32]; + let namespace_b = [2u8; 32]; + let key = [3u8; 32]; + let word = [4u8; 32]; + let other_word = [5u8; 32]; + + let mut store = StateStore::new(); + store.set_word(key, word); + store.set_namespaced_word(namespace_a, key, other_word); + store.set_namespaced_bytes(namespace_a, key, vec![1, 2, 3]); + + assert_eq!(store.get_word(key), word); + assert_eq!(store.get_namespaced_word(namespace_a, key), other_word); + assert_eq!(store.get_namespaced_word(namespace_b, key), [0u8; 32]); + assert_eq!(store.get_namespaced_bytes(namespace_a, key), vec![1, 2, 3]); + assert_eq!( + store.get_namespaced_bytes(namespace_b, key), + Vec::::new() + ); + + store.delete_namespaced(namespace_a, key); + assert_eq!(store.get_namespaced_word(namespace_a, key), [0u8; 32]); + assert_eq!(store.get_word(key), word); + + store.delete(key); + assert_eq!(store.get_word(key), [0u8; 32]); +} diff --git a/standards/dusk-contract-standards/tests/properties.proptest-regressions b/standards/dusk-contract-standards/tests/properties.proptest-regressions new file mode 100644 index 00000000..eb2ef747 --- /dev/null +++ b/standards/dusk-contract-standards/tests/properties.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 6f3227501a81e63efa3a96259de7dd04a69bd503e27f4953f5e298cd8a9b7294 # shrinks to ops = [Mint { to: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), amount: 98 }, Transfer { from: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), to: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), amount: 1 }] diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs new file mode 100644 index 00000000..4f15c37f --- /dev/null +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -0,0 +1,3415 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::collections::{BTreeMap, BTreeSet}; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use dusk_contract_standards::access::{ + AccessControl, Ownable2Step, OwnerSet, DEFAULT_ADMIN_ROLE, +}; +use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, AuthorizedAction, + MoonlightAuthorization, PhoenixSignatureAuthorization, SignedAuthorization, +}; +use dusk_contract_standards::core::{ + CallContext, NonceEntry, NonceManager, Principal, ReplayEntry, ReplayGuard, +}; +use dusk_contract_standards::governance::{ + MultisigController, MultisigControllerConfig, MultisigControllerStatus, + MultisigOperationId, MultisigPendingOperation, MultisigTarget, Timelock, + TimelockController, EXECUTOR_ROLE, PROPOSER_ROLE, +}; +use dusk_contract_standards::proxy::UpgradeAdmin; +use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, + Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, SupplyCap, + TransferCall as Transfer20, TransferFromCall, VotingUnits, +}; +use dusk_contract_standards::token::drc721::{ + ApproveCall as Approve721, BalanceOf as BalanceOf721, Drc721, GetApproved, + Init as Init721, InitToken, IsApprovedForAll, OwnerOf, RoyaltyInfo, + RoyaltyRegistry, SetApprovalForAllCall, TokensOf, + TransferFromCall as Transfer721, MAX_BASIS_POINTS, +}; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::transfer::data::ContractCall; +use dusk_core::JubJubScalar; +use proptest::prelude::*; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const TEST_CHAIN_ID: u8 = 0xD5; + +fn standards_proptest_config() -> ProptestConfig { + ProptestConfig { + cases: env_usize("STANDARDS_PROPTEST_CASES") + .or_else(|| env_usize("PROPTEST_CASES")) + .unwrap_or(256) as u32, + max_shrink_iters: env_usize("STANDARDS_PROPTEST_MAX_SHRINK_ITERS") + .or_else(|| env_usize("PROPTEST_MAX_SHRINK_ITERS")) + .unwrap_or(4096) as u32, + ..ProptestConfig::default() + } +} + +fn env_usize(name: &str) -> Option { + std::env::var(name).ok()?.parse().ok() +} + +fn principal(index: u8) -> Principal { + if index == 0 { + Principal::Contract(ContractId::from_bytes([0u8; 32])) + } else { + Principal::Phoenix([index; 32]) + } +} + +fn principal_strategy() -> BoxedStrategy { + (0u8..=5).prop_map(principal).boxed() +} + +fn all_principals() -> Vec { + (0u8..=5).map(principal).collect() +} + +fn contract_id(index: u8) -> ContractId { + ContractId::from_bytes([index; 32]) +} + +fn multisig_target( + contract_seed: u8, + arg_seed: u8, + salt_seed: u8, +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(contract_id(contract_seed), "set_value") + .with_raw_args(vec![arg_seed, arg_seed.wrapping_add(1)]), + salt: [salt_seed; 32], + } +} + +fn multisig_snapshot( + controller: &MultisigController, + id: MultisigOperationId, +) -> (Option, Option) { + (controller.proposal(id), controller.tombstone_expiry(id)) +} + +fn role(index: u8) -> [u8; 32] { + let mut role = [0u8; 32]; + role[0] = index; + role +} + +fn amount_strategy() -> BoxedStrategy { + prop_oneof![ + 12 => 0u64..=150, + 2 => (0u64..=16).prop_map(|delta| u64::MAX - delta), + 1 => Just(u64::MAX), + ] + .boxed() +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn phoenix_secret(seed: u64) -> SchnorrSecretKey { + SchnorrSecretKey::from(JubJubScalar::from(seed)) +} + +#[derive(Clone, Debug)] +enum Drc20Op { + Transfer { + from: Principal, + to: Principal, + amount: u64, + }, + Approve { + owner: Principal, + spender: Principal, + amount: u64, + }, + IncreaseAllowance { + owner: Principal, + spender: Principal, + amount: u64, + }, + DecreaseAllowance { + owner: Principal, + spender: Principal, + amount: u64, + }, + TransferFrom { + spender: Principal, + owner: Principal, + to: Principal, + amount: u64, + }, + Mint { + to: Principal, + amount: u64, + }, + Burn { + from: Principal, + amount: u64, + }, +} + +fn drc20_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let amount = amount_strategy(); + prop_oneof![ + (account.clone(), account.clone(), amount.clone()).prop_map( + |(from, to, amount)| Drc20Op::Transfer { from, to, amount }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::Approve { + owner, + spender, + amount, + }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + }, + ), + ( + account.clone(), + account.clone(), + account.clone(), + amount.clone() + ) + .prop_map(|(spender, owner, to, amount)| { + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } + }), + (account.clone(), amount.clone()) + .prop_map(|(to, amount)| Drc20Op::Mint { to, amount }), + (account, amount) + .prop_map(|(from, amount)| Drc20Op::Burn { from, amount }), + ] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Drc20Snapshot { + total_supply: u64, + balances: Vec<(Principal, u64)>, + allowances: Vec<(Principal, Principal, u64)>, +} + +#[derive(Clone, Debug)] +struct Drc20Model { + balances: BTreeMap, + allowances: BTreeMap<(Principal, Principal), u64>, + total_supply: u64, +} + +impl Drc20Model { + fn initial() -> Self { + let mut balances = BTreeMap::new(); + balances.insert(principal(1), 100); + balances.insert(principal(2), 50); + Self { + balances, + allowances: BTreeMap::new(), + total_supply: 150, + } + } + + fn snapshot(&self, accounts: &[Principal]) -> Drc20Snapshot { + let mut balances = Vec::new(); + let mut allowances = Vec::new(); + for &account in accounts { + balances.push((account, self.balance(account))); + } + for &owner in accounts { + for &spender in accounts { + allowances.push(( + owner, + spender, + self.allowance(owner, spender), + )); + } + } + Drc20Snapshot { + total_supply: self.total_supply, + balances, + allowances, + } + } + + fn balance(&self, account: Principal) -> u64 { + self.balances.get(&account).copied().unwrap_or(0) + } + + fn allowance(&self, owner: Principal, spender: Principal) -> u64 { + self.allowances.get(&(owner, spender)).copied().unwrap_or(0) + } + + fn set_balance(&mut self, account: Principal, amount: u64) { + if amount == 0 { + self.balances.remove(&account); + } else { + self.balances.insert(account, amount); + } + } + + fn apply(&mut self, op: &Drc20Op) -> bool { + match *op { + Drc20Op::Transfer { from, to, amount } => { + self.transfer(from, to, amount) + } + Drc20Op::Approve { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + self.allowances.insert((owner, spender), amount); + true + } + Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + let Some(next) = + self.allowance(owner, spender).checked_add(amount) + else { + return false; + }; + self.allowances.insert((owner, spender), next); + true + } + Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + let current = self.allowance(owner, spender); + if current < amount { + return false; + } + self.allowances.insert((owner, spender), current - amount); + true + } + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } => { + if spender.is_zero() || self.allowance(owner, spender) < amount + { + return false; + } + let before = self.clone(); + if !self.transfer(owner, to, amount) { + *self = before; + return false; + } + let current = self.allowance(owner, spender); + self.allowances.insert((owner, spender), current - amount); + true + } + Drc20Op::Mint { to, amount } => { + if to.is_zero() { + return false; + } + let Some(next_balance) = self.balance(to).checked_add(amount) + else { + return false; + }; + let Some(next_supply) = self.total_supply.checked_add(amount) + else { + return false; + }; + self.set_balance(to, next_balance); + self.total_supply = next_supply; + true + } + Drc20Op::Burn { from, amount } => { + if from.is_zero() || self.balance(from) < amount { + return false; + } + self.set_balance(from, self.balance(from) - amount); + self.total_supply -= amount; + true + } + } + } + + fn transfer( + &mut self, + from: Principal, + to: Principal, + amount: u64, + ) -> bool { + if from.is_zero() || to.is_zero() || self.balance(from) < amount { + return false; + } + if from != to && self.balance(to).checked_add(amount).is_none() { + return false; + } + if amount != 0 { + if from == to { + return true; + } + self.set_balance(from, self.balance(from) - amount); + self.set_balance(to, self.balance(to) + amount); + } + true + } +} + +fn drc20_snapshot(token: &Drc20, accounts: &[Principal]) -> Drc20Snapshot { + let mut balances = Vec::new(); + let mut allowances = Vec::new(); + for &account in accounts { + balances.push((account, token.balance_of(BalanceOf20 { account }))); + } + for &owner in accounts { + for &spender in accounts { + allowances.push(( + owner, + spender, + token.allowance(Allowance { owner, spender }), + )); + } + } + Drc20Snapshot { + total_supply: token.total_supply(), + balances, + allowances, + } +} + +fn apply_drc20_token(token: &mut Drc20, op: &Drc20Op) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Drc20Op::Transfer { from, to, amount } => { + token.transfer(from, Transfer20 { to, amount }); + } + Drc20Op::Approve { + owner, + spender, + amount, + } => { + token.approve(owner, ApproveCall { spender, amount }); + } + Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + } => { + token.increase_allowance( + owner, + IncreaseAllowanceCall { + spender, + added_amount: amount, + }, + ); + } + Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + } => { + token.decrease_allowance( + owner, + DecreaseAllowanceCall { + spender, + subtracted_amount: amount, + }, + ); + } + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } => { + token + .transfer_from(spender, TransferFromCall { owner, to, amount }); + } + Drc20Op::Mint { to, amount } => { + token.mint(to, amount); + } + Drc20Op::Burn { from, amount } => { + token.burn(from, amount); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum Drc721Op { + Mint { + to: Principal, + token_id: u64, + }, + Approve { + caller: Principal, + approved: Principal, + token_id: u64, + }, + SetApprovalForAll { + owner: Principal, + operator: Principal, + approved: bool, + }, + Transfer { + caller: Principal, + from: Principal, + to: Principal, + token_id: u64, + }, + Burn { + caller: Principal, + token_id: u64, + }, +} + +fn drc721_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let token_id = 0u64..=8; + prop_oneof![ + (account.clone(), token_id.clone()) + .prop_map(|(to, token_id)| Drc721Op::Mint { to, token_id }), + (account.clone(), account.clone(), token_id.clone()).prop_map( + |(caller, approved, token_id)| Drc721Op::Approve { + caller, + approved, + token_id, + }, + ), + (account.clone(), account.clone(), any::()).prop_map( + |(owner, operator, approved)| Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + }, + ), + ( + account.clone(), + account.clone(), + account.clone(), + token_id.clone() + ) + .prop_map(|(caller, from, to, token_id)| { + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } + }), + (account, token_id) + .prop_map(|(caller, token_id)| Drc721Op::Burn { caller, token_id }), + ] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Drc721Snapshot { + total_supply: u64, + owners: Vec<(u64, Option)>, + balances: Vec<(Principal, u64)>, + approvals: Vec<(u64, Option)>, + operators: Vec<(Principal, Principal, bool)>, + tokens: Vec<(Principal, Vec)>, +} + +#[derive(Clone, Debug)] +struct Drc721Model { + owners: BTreeMap, + approvals: BTreeMap, + operators: BTreeMap<(Principal, Principal), bool>, +} + +impl Drc721Model { + fn initial() -> Self { + let mut owners = BTreeMap::new(); + owners.insert(1, principal(1)); + owners.insert(2, principal(2)); + Self { + owners, + approvals: BTreeMap::new(), + operators: BTreeMap::new(), + } + } + + fn snapshot( + &self, + accounts: &[Principal], + token_ids: &[u64], + ) -> Drc721Snapshot { + let owners = token_ids + .iter() + .map(|&id| (id, self.owners.get(&id).copied())) + .collect(); + let balances = accounts + .iter() + .copied() + .filter(|account| !account.is_zero()) + .map(|account| (account, self.balance(account))) + .collect(); + let approvals = token_ids + .iter() + .map(|&id| (id, self.approved(id))) + .collect(); + let mut operators = Vec::new(); + for &owner in accounts { + for &operator in accounts { + operators.push(( + owner, + operator, + self.is_operator(owner, operator), + )); + } + } + let tokens = accounts + .iter() + .copied() + .map(|account| (account, self.tokens_of(account))) + .collect(); + Drc721Snapshot { + total_supply: self.owners.len() as u64, + owners, + balances, + approvals, + operators, + tokens, + } + } + + fn balance(&self, account: Principal) -> u64 { + self.owners + .values() + .filter(|&&owner| owner == account) + .count() as u64 + } + + fn approved(&self, token_id: u64) -> Option { + if self.owners.contains_key(&token_id) { + Some( + self.approvals + .get(&token_id) + .copied() + .unwrap_or_else(|| principal(0)), + ) + } else { + None + } + } + + fn is_operator(&self, owner: Principal, operator: Principal) -> bool { + self.operators + .get(&(owner, operator)) + .copied() + .unwrap_or(false) + } + + fn tokens_of(&self, account: Principal) -> Vec { + self.owners + .iter() + .filter_map(|(&token_id, &owner)| { + if owner == account { + Some(token_id) + } else { + None + } + }) + .collect() + } + + fn apply(&mut self, op: &Drc721Op) -> bool { + match *op { + Drc721Op::Mint { to, token_id } => { + if to.is_zero() || self.owners.contains_key(&token_id) { + return false; + } + self.owners.insert(token_id, to); + true + } + Drc721Op::Approve { + caller, + approved, + token_id, + } => { + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + if approved == owner { + return false; + } + if caller != owner && !self.is_operator(owner, caller) { + return false; + } + if approved.is_zero() { + self.approvals.remove(&token_id); + } else { + self.approvals.insert(token_id, approved); + } + true + } + Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + } => { + if owner.is_zero() || operator.is_zero() || operator == owner { + return false; + } + self.operators.insert((owner, operator), approved); + true + } + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } => { + if to.is_zero() { + return false; + } + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + if owner != from { + return false; + } + let approved = self.approvals.get(&token_id).copied(); + if caller != owner + && approved != Some(caller) + && !self.is_operator(owner, caller) + { + return false; + } + self.approvals.remove(&token_id); + self.owners.insert(token_id, to); + true + } + Drc721Op::Burn { caller, token_id } => { + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + let approved = self.approvals.get(&token_id).copied(); + if caller != owner + && approved != Some(caller) + && !self.is_operator(owner, caller) + { + return false; + } + self.owners.remove(&token_id); + self.approvals.remove(&token_id); + true + } + } + } +} + +fn drc721_snapshot( + token: &Drc721, + accounts: &[Principal], + token_ids: &[u64], +) -> Drc721Snapshot { + let owners = token_ids + .iter() + .map(|&token_id| { + let owner = catch_unwind(AssertUnwindSafe(|| { + token.owner_of(OwnerOf { token_id }) + })) + .ok(); + (token_id, owner) + }) + .collect(); + let balances = accounts + .iter() + .copied() + .filter(|account| !account.is_zero()) + .map(|account| (account, token.balance_of(BalanceOf721 { account }))) + .collect(); + let approvals = token_ids + .iter() + .map(|&token_id| { + let approval = catch_unwind(AssertUnwindSafe(|| { + token.get_approved(GetApproved { token_id }) + })) + .ok(); + (token_id, approval) + }) + .collect(); + let mut operators = Vec::new(); + for &owner in accounts { + for &operator in accounts { + operators.push(( + owner, + operator, + token.is_approved_for_all(IsApprovedForAll { owner, operator }), + )); + } + } + let tokens = accounts + .iter() + .copied() + .map(|owner| (owner, token.tokens_of(TokensOf { owner }))) + .collect(); + Drc721Snapshot { + total_supply: token.total_supply(), + owners, + balances, + approvals, + operators, + tokens, + } +} + +fn apply_drc721_token(token: &mut Drc721, op: &Drc721Op) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Drc721Op::Mint { to, token_id } => { + token.mint(to, token_id); + } + Drc721Op::Approve { + caller, + approved, + token_id, + } => { + token.approve(caller, Approve721 { approved, token_id }); + } + Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + } => { + token.set_approval_for_all( + owner, + SetApprovalForAllCall { operator, approved }, + ); + } + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } => { + token.transfer_from(caller, Transfer721 { from, to, token_id }); + } + Drc721Op::Burn { caller, token_id } => { + token.burn(caller, token_id); + } + })) + .is_ok() +} + +#[derive(Clone, Copy, Debug)] +enum AuthCase { + Good, + WrongChain, + WrongContract, + WrongDomain, + WrongAction, + WrongPayload, + Expired, + FutureNonce, + BadPrincipal, + BadSignature, + WrongExpected, + OwnerSetNonOwner, + RoleNonMember, + AdminMismatch, +} + +fn auth_case_strategy() -> impl Strategy { + prop_oneof![ + Just(AuthCase::Good), + Just(AuthCase::WrongChain), + Just(AuthCase::WrongContract), + Just(AuthCase::WrongDomain), + Just(AuthCase::WrongAction), + Just(AuthCase::WrongPayload), + Just(AuthCase::Expired), + Just(AuthCase::FutureNonce), + Just(AuthCase::BadPrincipal), + Just(AuthCase::BadSignature), + Just(AuthCase::WrongExpected), + Just(AuthCase::OwnerSetNonOwner), + Just(AuthCase::RoleNonMember), + Just(AuthCase::AdminMismatch), + ] +} + +fn signed_action( + secret: &BlsSecretKey, + public: BlsPublicKey, + action: AuthorizedAction, +) -> SignedAuthorization { + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: public, + signature: secret.sign(&action.message_bytes()), + }) +} + +#[derive(Clone, Copy, Debug)] +enum PhoenixAuthCase { + Good, + WrongChain, + WrongContract, + WrongDomain, + WrongAction, + WrongPayload, + Expired, + FutureNonce, + BadPrincipal, + BadSignature, + WrongExpected, + OwnerSetNonOwner, + RoleNonMember, + AdminMismatch, + ReplayKeyUsed, +} + +fn phoenix_auth_case_strategy() -> impl Strategy { + prop_oneof![ + Just(PhoenixAuthCase::Good), + Just(PhoenixAuthCase::WrongChain), + Just(PhoenixAuthCase::WrongContract), + Just(PhoenixAuthCase::WrongDomain), + Just(PhoenixAuthCase::WrongAction), + Just(PhoenixAuthCase::WrongPayload), + Just(PhoenixAuthCase::Expired), + Just(PhoenixAuthCase::FutureNonce), + Just(PhoenixAuthCase::BadPrincipal), + Just(PhoenixAuthCase::BadSignature), + Just(PhoenixAuthCase::WrongExpected), + Just(PhoenixAuthCase::OwnerSetNonOwner), + Just(PhoenixAuthCase::RoleNonMember), + Just(PhoenixAuthCase::AdminMismatch), + Just(PhoenixAuthCase::ReplayKeyUsed), + ] +} + +fn phoenix_signed_action( + secret: &SchnorrSecretKey, + public: SchnorrPublicKey, + action: AuthorizedAction, + replay_key: Option<[u8; 32]>, + seed: u64, +) -> SignedAuthorization { + let mut rng = StdRng::seed_from_u64(seed); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key: public, + signature: secret.sign(&mut rng, action.message_hash()), + replay_key, + }) +} + +fn authorization_probe(case: AuthCase) -> (bool, u64) { + let secret = moonlight_secret(31); + let public = BlsPublicKey::from(&secret); + let signer = Principal::moonlight(&public); + let contract = contract_id(32); + let domain = [33u8; 32]; + let action_id = [34u8; 32]; + let payload_hash = [35u8; 32]; + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); + let mut action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: signer, + payload_hash, + }; + let mut now = 100; + let expected = match case { + AuthCase::Good => signer, + AuthCase::WrongExpected => principal(1), + AuthCase::WrongChain => { + action.chain_id = TEST_CHAIN_ID.wrapping_add(1); + signer + } + AuthCase::WrongContract => { + action.contract = contract_id(36); + signer + } + AuthCase::WrongDomain => { + action.domain = [37u8; 32]; + signer + } + AuthCase::WrongAction => { + action.action_id = [38u8; 32]; + signer + } + AuthCase::WrongPayload => { + action.payload_hash = [39u8; 32]; + signer + } + AuthCase::Expired => { + now = 101; + signer + } + AuthCase::FutureNonce => { + action.nonce = 1; + signer + } + AuthCase::BadPrincipal => { + action.principal = principal(2); + signer + } + AuthCase::BadSignature + | AuthCase::OwnerSetNonOwner + | AuthCase::RoleNonMember + | AuthCase::AdminMismatch => signer, + }; + + let signed = if matches!(case, AuthCase::BadSignature) { + let original = action; + action.payload_hash = [42u8; 32]; + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: public, + signature: secret.sign(&original.message_bytes()), + }) + } else { + signed_action(&secret, public, action) + }; + + let mut authorizations = AuthorizationManager::new(); + let result = catch_unwind(AssertUnwindSafe(|| match case { + AuthCase::OwnerSetNonOwner => { + let mut owners = OwnerSet::new(); + owners.init([principal(1)]); + owners.authorize_owner_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + AuthCase::RoleNonMember => { + let checked_role = role(40); + let mut access = AccessControl::new(); + access.init_admin(principal(1)); + access.grant_role(principal(1), checked_role, principal(2)); + access.authorize_role_action( + checked_role, + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + AuthCase::AdminMismatch => { + let upgrade = + UpgradeAdmin::new(principal(1), contract_id(41), 0, 0); + upgrade.authorize_admin_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + _ => { + authorizations.authorize_principal_action( + expected, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + })) + .is_ok(); + (result, authorizations.nonce(signer, domain)) +} + +fn phoenix_authorization_probe(case: PhoenixAuthCase) -> (bool, u64, bool) { + let secret = phoenix_secret(43); + let public = SchnorrPublicKey::from(&secret); + let signer = Principal::phoenix_public_key(&public); + let contract = contract_id(44); + let domain = [45u8; 32]; + let action_id = [46u8; 32]; + let payload_hash = [47u8; 32]; + let replay_key = [48u8; 32]; + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); + let mut action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: signer, + payload_hash, + }; + let mut now = 100; + let expected = match case { + PhoenixAuthCase::Good | PhoenixAuthCase::ReplayKeyUsed => signer, + PhoenixAuthCase::WrongExpected => principal(1), + PhoenixAuthCase::WrongChain => { + action.chain_id = TEST_CHAIN_ID.wrapping_add(1); + signer + } + PhoenixAuthCase::WrongContract => { + action.contract = contract_id(49); + signer + } + PhoenixAuthCase::WrongDomain => { + action.domain = [50u8; 32]; + signer + } + PhoenixAuthCase::WrongAction => { + action.action_id = [51u8; 32]; + signer + } + PhoenixAuthCase::WrongPayload => { + action.payload_hash = [52u8; 32]; + signer + } + PhoenixAuthCase::Expired => { + now = 101; + signer + } + PhoenixAuthCase::FutureNonce => { + action.nonce = 1; + signer + } + PhoenixAuthCase::BadPrincipal => { + action.principal = principal(2); + signer + } + PhoenixAuthCase::BadSignature + | PhoenixAuthCase::OwnerSetNonOwner + | PhoenixAuthCase::RoleNonMember + | PhoenixAuthCase::AdminMismatch => signer, + }; + + let signed = if matches!(case, PhoenixAuthCase::BadSignature) { + let original = action; + action.payload_hash = [53u8; 32]; + let mut rng = StdRng::seed_from_u64(55); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key: public, + signature: secret.sign(&mut rng, original.message_hash()), + replay_key: Some(replay_key), + }) + } else { + phoenix_signed_action(&secret, public, action, Some(replay_key), 56) + }; + + let mut authorizations = AuthorizationManager::new(); + if matches!(case, PhoenixAuthCase::ReplayKeyUsed) { + authorizations.import_replay_entries([ReplayEntry { + principal: signer, + key: replay_key, + }]); + } + + let result = catch_unwind(AssertUnwindSafe(|| match case { + PhoenixAuthCase::OwnerSetNonOwner => { + let mut owners = OwnerSet::new(); + owners.init([principal(1)]); + owners.authorize_owner_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + PhoenixAuthCase::RoleNonMember => { + let checked_role = role(57); + let mut access = AccessControl::new(); + access.init_admin(principal(1)); + access.grant_role(principal(1), checked_role, principal(2)); + access.authorize_role_action( + checked_role, + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + PhoenixAuthCase::AdminMismatch => { + let upgrade = + UpgradeAdmin::new(principal(1), contract_id(58), 0, 0); + upgrade.authorize_admin_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + _ => { + authorizations.authorize_principal_action( + expected, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + })) + .is_ok(); + ( + result, + authorizations.nonce(signer, domain), + authorizations.replay_used(signer, replay_key), + ) +} + +#[derive(Clone, Debug)] +enum OwnerSetOp { + Add { + caller: Principal, + owner: Principal, + }, + Remove { + caller: Principal, + owner: Principal, + }, + Replace { + caller: Principal, + old_owner: Principal, + new_owner: Principal, + }, +} + +fn owner_set_op_strategy() -> impl Strategy { + let account = principal_strategy(); + prop_oneof![ + (account.clone(), account.clone()) + .prop_map(|(caller, owner)| { OwnerSetOp::Add { caller, owner } }), + (account.clone(), account.clone()).prop_map(|(caller, owner)| { + OwnerSetOp::Remove { caller, owner } + }), + (account.clone(), account.clone(), account).prop_map( + |(caller, old_owner, new_owner)| OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + }, + ), + ] +} + +fn apply_owner_set_model( + owners: &mut BTreeSet, + op: &OwnerSetOp, +) -> bool { + match *op { + OwnerSetOp::Add { caller, owner } => { + if !owners.contains(&caller) || owner.is_zero() { + return false; + } + owners.insert(owner); + true + } + OwnerSetOp::Remove { caller, owner } => { + if !owners.contains(&caller) + || !owners.contains(&owner) + || owners.len() == 1 + { + return false; + } + owners.remove(&owner); + true + } + OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + } => { + if !owners.contains(&caller) + || new_owner.is_zero() + || !owners.contains(&old_owner) + || (old_owner != new_owner && owners.contains(&new_owner)) + { + return false; + } + owners.remove(&old_owner); + owners.insert(new_owner); + true + } + } +} + +fn apply_owner_set(owners: &mut OwnerSet, op: &OwnerSetOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + OwnerSetOp::Add { caller, owner } => owners.add_owner(caller, owner), + OwnerSetOp::Remove { caller, owner } => { + owners.remove_owner(caller, owner) + } + OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + } => owners.replace_owner(caller, old_owner, new_owner), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum Ownable2StepOp { + Transfer { + caller: Principal, + new_owner: Principal, + }, + Accept { + caller: Principal, + }, + Renounce { + caller: Principal, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Ownable2StepSnapshot { + owner: Option, + pending_owner: Option, +} + +#[derive(Clone, Copy, Debug)] +struct Ownable2StepModel { + owner: Option, + pending_owner: Option, +} + +impl Ownable2StepModel { + fn new(owner: Principal) -> Self { + Self { + owner: Some(owner), + pending_owner: None, + } + } + + fn apply(&mut self, op: &Ownable2StepOp) -> bool { + match *op { + Ownable2StepOp::Transfer { caller, new_owner } => { + if self.owner != Some(caller) || new_owner.is_zero() { + return false; + } + self.pending_owner = Some(new_owner); + true + } + Ownable2StepOp::Accept { caller } => { + if self.pending_owner != Some(caller) { + return false; + } + self.owner = Some(caller); + self.pending_owner = None; + true + } + Ownable2StepOp::Renounce { caller } => { + if self.owner != Some(caller) { + return false; + } + self.owner = None; + self.pending_owner = None; + true + } + } + } + + const fn snapshot(&self) -> Ownable2StepSnapshot { + Ownable2StepSnapshot { + owner: self.owner, + pending_owner: self.pending_owner, + } + } +} + +fn ownable2_step_op_strategy() -> impl Strategy { + let account = principal_strategy(); + prop_oneof![ + (account.clone(), account.clone()).prop_map(|(caller, new_owner)| { + Ownable2StepOp::Transfer { caller, new_owner } + },), + account + .clone() + .prop_map(|caller| Ownable2StepOp::Accept { caller }), + account.prop_map(|caller| Ownable2StepOp::Renounce { caller }), + ] +} + +fn ownable2_step_snapshot(ownable: &Ownable2Step) -> Ownable2StepSnapshot { + Ownable2StepSnapshot { + owner: ownable.owner(), + pending_owner: ownable.pending_owner(), + } +} + +fn apply_ownable2_step( + ownable: &mut Ownable2Step, + op: &Ownable2StepOp, +) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Ownable2StepOp::Transfer { caller, new_owner } => { + ownable.transfer_ownership(caller, new_owner); + } + Ownable2StepOp::Accept { caller } => { + ownable.accept_ownership(caller); + } + Ownable2StepOp::Renounce { caller } => { + ownable.renounce_ownership(caller); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum AccessOp { + Grant { + caller: Principal, + role: [u8; 32], + account: Principal, + }, + Revoke { + caller: Principal, + role: [u8; 32], + account: Principal, + }, + Renounce { + role: [u8; 32], + caller: Principal, + }, + SetRoleAdmin { + caller: Principal, + role: [u8; 32], + admin_role: [u8; 32], + }, +} + +#[derive(Clone, Debug)] +struct RoleModel { + members: BTreeSet, + admin_role: [u8; 32], +} + +#[derive(Clone, Debug)] +struct AccessModel { + roles: BTreeMap<[u8; 32], RoleModel>, +} + +impl AccessModel { + fn new(admin: Principal) -> Self { + let mut members = BTreeSet::new(); + members.insert(admin); + let mut roles = BTreeMap::new(); + roles.insert( + DEFAULT_ADMIN_ROLE, + RoleModel { + members, + admin_role: DEFAULT_ADMIN_ROLE, + }, + ); + Self { roles } + } + + fn has_role(&self, role: [u8; 32], account: Principal) -> bool { + self.roles + .get(&role) + .map(|role| role.members.contains(&account)) + .unwrap_or(false) + } + + fn role_admin(&self, role: [u8; 32]) -> [u8; 32] { + self.roles + .get(&role) + .map(|role| role.admin_role) + .unwrap_or(DEFAULT_ADMIN_ROLE) + } + + fn apply(&mut self, op: &AccessOp) -> bool { + match *op { + AccessOp::Grant { + caller, + role, + account, + } => { + if account.is_zero() + || !self.has_role(self.role_admin(role), caller) + { + return false; + } + self.roles + .entry(role) + .or_insert_with(|| RoleModel { + members: BTreeSet::new(), + admin_role: DEFAULT_ADMIN_ROLE, + }) + .members + .insert(account); + true + } + AccessOp::Revoke { + caller, + role, + account, + } => { + if !self.has_role(self.role_admin(role), caller) { + return false; + } + if let Some(role) = self.roles.get_mut(&role) { + role.members.remove(&account); + } + true + } + AccessOp::Renounce { role, caller } => { + if let Some(role) = self.roles.get_mut(&role) { + role.members.remove(&caller); + } + true + } + AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + } => { + if !self.has_role(self.role_admin(role), caller) { + return false; + } + self.roles + .entry(role) + .or_insert_with(|| RoleModel { + members: BTreeSet::new(), + admin_role: DEFAULT_ADMIN_ROLE, + }) + .admin_role = admin_role; + true + } + } + } + + fn snapshot( + &self, + roles: &[[u8; 32]], + accounts: &[Principal], + ) -> AccessSnapshot { + AccessSnapshot { + admins: roles + .iter() + .copied() + .map(|role| (role, self.role_admin(role))) + .collect(), + memberships: roles + .iter() + .copied() + .flat_map(|role| { + accounts.iter().copied().map(move |account| { + (role, account, self.has_role(role, account)) + }) + }) + .collect(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct AccessSnapshot { + admins: Vec<([u8; 32], [u8; 32])>, + memberships: Vec<([u8; 32], Principal, bool)>, +} + +fn access_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let role_strategy = + (0u8..=3).prop_map( + |i| { + if i == 0 { + DEFAULT_ADMIN_ROLE + } else { + role(i) + } + }, + ); + prop_oneof![ + (account.clone(), role_strategy.clone(), account.clone()).prop_map( + |(caller, role, account)| AccessOp::Grant { + caller, + role, + account, + }, + ), + (account.clone(), role_strategy.clone(), account.clone()).prop_map( + |(caller, role, account)| AccessOp::Revoke { + caller, + role, + account, + }, + ), + (role_strategy.clone(), account.clone()) + .prop_map(|(role, caller)| AccessOp::Renounce { role, caller },), + (account, role_strategy.clone(), role_strategy).prop_map( + |(caller, role, admin_role)| AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + }, + ), + ] +} + +fn access_snapshot( + access: &AccessControl, + roles: &[[u8; 32]], + accounts: &[Principal], +) -> AccessSnapshot { + AccessSnapshot { + admins: roles + .iter() + .copied() + .map(|role| (role, access.get_role_admin(role))) + .collect(), + memberships: roles + .iter() + .copied() + .flat_map(|role| { + accounts.iter().copied().map(move |account| { + (role, account, access.has_role(role, account)) + }) + }) + .collect(), + } +} + +fn apply_access(access: &mut AccessControl, op: &AccessOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + AccessOp::Grant { + caller, + role, + account, + } => access.grant_role(caller, role, account), + AccessOp::Revoke { + caller, + role, + account, + } => access.revoke_role(caller, role, account), + AccessOp::Renounce { role, caller } => { + access.renounce_role(role, caller) + } + AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + } => access.set_role_admin(caller, role, admin_role), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum TimelockOp { + SetDelay { + delay: u64, + }, + Schedule { + id: [u8; 32], + now: u64, + payload: Vec, + }, + Cancel { + id: [u8; 32], + }, + Execute { + id: [u8; 32], + now: u64, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct TimelockSnapshot { + min_delay: u64, + operations: Vec, +} + +type TimelockOperationSnapshot = ([u8; 32], Option<(u64, bool, Vec)>); + +#[derive(Clone, Debug)] +struct TimelockModel { + min_delay: u64, + operations: BTreeMap<[u8; 32], (u64, bool, Vec)>, +} + +impl TimelockModel { + fn new(min_delay: u64) -> Self { + Self { + min_delay, + operations: BTreeMap::new(), + } + } + + fn apply(&mut self, op: &TimelockOp) -> bool { + match op { + TimelockOp::SetDelay { delay } => { + self.min_delay = *delay; + true + } + TimelockOp::Schedule { id, now, payload } => { + if self.operations.contains_key(id) { + return false; + } + let Some(ready_at) = now.checked_add(self.min_delay) else { + return false; + }; + self.operations + .insert(*id, (ready_at, false, payload.clone())); + true + } + TimelockOp::Cancel { id } => { + let Some((_, done, _)) = self.operations.get(id) else { + return false; + }; + if *done { + return false; + } + self.operations.remove(id); + true + } + TimelockOp::Execute { id, now } => { + let Some((ready_at, done, _)) = self.operations.get_mut(id) + else { + return false; + }; + if *done || *now < *ready_at { + return false; + } + *done = true; + true + } + } + } + + fn snapshot(&self, ids: &[[u8; 32]]) -> TimelockSnapshot { + TimelockSnapshot { + min_delay: self.min_delay, + operations: ids + .iter() + .copied() + .map(|id| (id, self.operations.get(&id).cloned())) + .collect(), + } + } +} + +fn timelock_op_strategy() -> impl Strategy { + let id = (0u8..=5).prop_map(|i| [i; 32]); + let now = prop_oneof![0u64..=64, (0u64..=8).prop_map(|d| u64::MAX - d)]; + let payload = prop::collection::vec(0u8..=5, 0..4); + prop_oneof![ + (0u64..=16).prop_map(|delay| TimelockOp::SetDelay { delay }), + (id.clone(), now.clone(), payload).prop_map(|(id, now, payload)| { + TimelockOp::Schedule { id, now, payload } + },), + id.clone().prop_map(|id| TimelockOp::Cancel { id }), + (id, now).prop_map(|(id, now)| TimelockOp::Execute { id, now }), + ] +} + +fn timelock_snapshot( + timelock: &Timelock, + ids: &[[u8; 32]], +) -> TimelockSnapshot { + TimelockSnapshot { + min_delay: timelock.min_delay(), + operations: ids + .iter() + .copied() + .map(|id| { + ( + id, + timelock + .get(id) + .map(|op| (op.ready_at, op.done, op.payload.clone())), + ) + }) + .collect(), + } +} + +fn apply_timelock(timelock: &mut Timelock, op: &TimelockOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + TimelockOp::SetDelay { delay } => timelock.set_min_delay(*delay), + TimelockOp::Schedule { id, now, payload } => { + timelock.schedule(*id, *now, payload.clone()); + } + TimelockOp::Cancel { id } => timelock.cancel(*id), + TimelockOp::Execute { id, now } => { + timelock.execute(*id, *now); + } + })) + .is_ok() +} + +fn bad_min_delay_payload_strategy() -> impl Strategy> { + prop_oneof![ + prop::collection::vec(any::(), 0..8), + prop::collection::vec(any::(), 9..17), + ] +} + +#[derive(Clone, Debug)] +enum UpgradeOp { + Prepare { + caller: Principal, + implementation: ContractId, + now: u64, + payload: Vec, + }, + Activate { + caller: Principal, + now: u64, + }, + Cancel { + caller: Principal, + }, + Rollback { + caller: Principal, + now: u64, + }, + Finalize { + caller: Principal, + now: u64, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct UpgradeSnapshot { + implementation: ContractId, + pending: Option<(ContractId, u64, Vec)>, + rollback_deadline: u64, +} + +#[derive(Clone, Debug)] +struct UpgradeModel { + admin: Principal, + implementation: ContractId, + previous: Option, + pending: Option<(ContractId, u64, Vec)>, + delay: u64, + rollback_window: u64, + rollback_deadline: u64, +} + +impl UpgradeModel { + fn new(admin: Principal, implementation: ContractId) -> Self { + Self { + admin, + implementation, + previous: None, + pending: None, + delay: 5, + rollback_window: 10, + rollback_deadline: 0, + } + } + + fn apply(&mut self, op: &UpgradeOp) -> bool { + match op { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } => { + if *caller != self.admin || is_zero_contract_id(*implementation) + { + return false; + } + let Some(eta) = now.checked_add(self.delay) else { + return false; + }; + self.pending = Some((*implementation, eta, payload.clone())); + true + } + UpgradeOp::Activate { caller, now } => { + if *caller != self.admin { + return false; + } + let Some((implementation, eta, _)) = self.pending.clone() + else { + return false; + }; + if *now < eta { + return false; + } + let Some(deadline) = now.checked_add(self.rollback_window) + else { + return false; + }; + self.previous = Some(self.implementation); + self.implementation = implementation; + self.pending = None; + self.rollback_deadline = deadline; + true + } + UpgradeOp::Cancel { caller } => { + if *caller != self.admin || self.pending.is_none() { + return false; + } + self.pending = None; + true + } + UpgradeOp::Rollback { caller, now } => { + if *caller != self.admin + || self.rollback_deadline == 0 + || *now > self.rollback_deadline + { + return false; + } + let Some(previous) = self.previous.take() else { + return false; + }; + self.implementation = previous; + self.pending = None; + self.rollback_deadline = 0; + true + } + UpgradeOp::Finalize { caller, now } => { + if *caller != self.admin + || (self.rollback_deadline != 0 + && *now < self.rollback_deadline) + { + return false; + } + self.previous = None; + self.rollback_deadline = 0; + true + } + } + } + + fn snapshot(&self) -> UpgradeSnapshot { + UpgradeSnapshot { + implementation: self.implementation, + pending: self.pending.clone(), + rollback_deadline: self.rollback_deadline, + } + } +} + +fn upgrade_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let implementation = (0u8..=5).prop_map(contract_id); + let now = prop_oneof![0u64..=32, (0u64..=12).prop_map(|d| u64::MAX - d)]; + let payload = prop::collection::vec(0u8..=9, 0..4); + prop_oneof![ + (account.clone(), implementation, now.clone(), payload).prop_map( + |(caller, implementation, now, payload)| { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } + } + ), + (account.clone(), now.clone()) + .prop_map(|(caller, now)| { UpgradeOp::Activate { caller, now } }), + account + .clone() + .prop_map(|caller| UpgradeOp::Cancel { caller }), + (account.clone(), now.clone()) + .prop_map(|(caller, now)| { UpgradeOp::Rollback { caller, now } }), + (account, now) + .prop_map(|(caller, now)| UpgradeOp::Finalize { caller, now }), + ] +} + +fn upgrade_snapshot(upgrade: &UpgradeAdmin) -> UpgradeSnapshot { + UpgradeSnapshot { + implementation: upgrade.implementation(), + pending: upgrade.pending().map(|pending| { + ( + pending.implementation, + pending.eta, + pending.migrate_data.clone(), + ) + }), + rollback_deadline: upgrade.rollback_deadline(), + } +} + +fn apply_upgrade(upgrade: &mut UpgradeAdmin, op: &UpgradeOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } => { + upgrade.prepare(*caller, *implementation, *now, payload.clone()); + } + UpgradeOp::Activate { caller, now } => { + upgrade.activate(*caller, *now); + } + UpgradeOp::Cancel { caller } => { + upgrade.cancel_pending(*caller); + } + UpgradeOp::Rollback { caller, now } => { + upgrade.rollback(*caller, *now); + } + UpgradeOp::Finalize { caller, now } => { + upgrade.finalize_rollback_window(*caller, *now); + } + })) + .is_ok() +} + +fn is_zero_contract_id(id: ContractId) -> bool { + id.to_bytes().iter().all(|byte| *byte == 0) +} + +#[derive(Clone, Debug)] +enum NonceReplayOp { + Consume { + principal: Principal, + domain: [u8; 32], + nonce: u64, + }, + UseNext { + principal: Principal, + domain: [u8; 32], + }, + InvalidateUntil { + principal: Principal, + domain: [u8; 32], + nonce: u64, + }, + ImportNonce { + entries: Vec, + }, + ConsumeReplay { + principal: Principal, + key: [u8; 32], + }, + ImportReplay { + entries: Vec, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct NonceReplaySnapshot { + nonces: Vec, + replay_entries: Vec, +} + +#[derive(Clone, Debug, Default)] +struct NonceReplayModel { + nonces: BTreeMap<(Principal, [u8; 32]), u64>, + replay_entries: BTreeSet<(Principal, [u8; 32])>, +} + +impl NonceReplayModel { + fn current(&self, principal: Principal, domain: [u8; 32]) -> u64 { + self.nonces.get(&(principal, domain)).copied().unwrap_or(0) + } + + fn apply(&mut self, op: &NonceReplayOp) -> bool { + match op { + NonceReplayOp::Consume { + principal, + domain, + nonce, + } => { + if self.current(*principal, *domain) != *nonce { + return false; + } + let Some(next) = nonce.checked_add(1) else { + return false; + }; + self.nonces.insert((*principal, *domain), next); + true + } + NonceReplayOp::UseNext { principal, domain } => { + let current = self.current(*principal, *domain); + let Some(next) = current.checked_add(1) else { + return false; + }; + self.nonces.insert((*principal, *domain), next); + true + } + NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + } => { + if *nonce < self.current(*principal, *domain) { + return false; + } + self.nonces.insert((*principal, *domain), *nonce); + true + } + NonceReplayOp::ImportNonce { entries } => { + for entry in entries { + let current = self.current(entry.principal, entry.domain); + if entry.nonce > current { + self.nonces.insert( + (entry.principal, entry.domain), + entry.nonce, + ); + } + } + true + } + NonceReplayOp::ConsumeReplay { principal, key } => { + self.replay_entries.insert((*principal, *key)) + } + NonceReplayOp::ImportReplay { entries } => { + for entry in entries { + self.replay_entries.insert((entry.principal, entry.key)); + } + true + } + } + } + + fn snapshot(&self) -> NonceReplaySnapshot { + NonceReplaySnapshot { + nonces: self + .nonces + .iter() + .map(|((principal, domain), nonce)| NonceEntry { + principal: *principal, + domain: *domain, + nonce: *nonce, + }) + .collect(), + replay_entries: self + .replay_entries + .iter() + .map(|(principal, key)| ReplayEntry { + principal: *principal, + key: *key, + }) + .collect(), + } + } +} + +fn nonce_replay_op_strategy() -> impl Strategy { + let principal = principal_strategy(); + let domain = (0u8..=4).prop_map(|i| [i; 32]); + let nonce = prop_oneof![ + 12 => 0u64..=12, + 1 => Just(u64::MAX), + 1 => (0u64..=4).prop_map(|d| u64::MAX - d), + ]; + let nonce_entry = (principal.clone(), domain.clone(), nonce.clone()) + .prop_map(|(principal, domain, nonce)| NonceEntry { + principal, + domain, + nonce, + }); + let replay_entry = (principal.clone(), domain.clone()) + .prop_map(|(principal, key)| ReplayEntry { principal, key }); + prop_oneof![ + (principal.clone(), domain.clone(), nonce.clone()).prop_map( + |(principal, domain, nonce)| NonceReplayOp::Consume { + principal, + domain, + nonce, + } + ), + (principal.clone(), domain.clone()).prop_map(|(principal, domain)| { + NonceReplayOp::UseNext { principal, domain } + },), + (principal.clone(), domain.clone(), nonce).prop_map( + |(principal, domain, nonce)| NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + }, + ), + prop::collection::vec(nonce_entry, 0..5) + .prop_map(|entries| NonceReplayOp::ImportNonce { entries }), + (principal, domain).prop_map(|(principal, key)| { + NonceReplayOp::ConsumeReplay { principal, key } + }), + prop::collection::vec(replay_entry, 0..5) + .prop_map(|entries| NonceReplayOp::ImportReplay { entries }), + ] +} + +fn nonce_replay_snapshot( + nonces: &NonceManager, + replays: &ReplayGuard, +) -> NonceReplaySnapshot { + NonceReplaySnapshot { + nonces: nonces.entries(), + replay_entries: replays.entries(), + } +} + +fn apply_nonce_replay( + nonces: &mut NonceManager, + replays: &mut ReplayGuard, + op: &NonceReplayOp, +) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + NonceReplayOp::Consume { + principal, + domain, + nonce, + } => { + nonces.consume(*principal, *domain, *nonce); + } + NonceReplayOp::UseNext { principal, domain } => { + nonces.use_next(*principal, *domain); + } + NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + } => nonces.invalidate_until(*principal, *domain, *nonce), + NonceReplayOp::ImportNonce { entries } => { + nonces.import_entries(entries.iter().copied()); + } + NonceReplayOp::ConsumeReplay { principal, key } => { + replays.consume(*principal, *key); + } + NonceReplayOp::ImportReplay { entries } => { + replays.import_entries(entries.iter().copied()); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum VotingOp { + Move { + from: Option, + to: Option, + amount: u64, + timepoint: u64, + }, + Write { + account: Principal, + timepoint: u64, + value: u64, + }, +} + +#[derive(Clone, Debug, Default)] +struct CheckpointModel { + entries: Vec<(u64, u64)>, +} + +impl CheckpointModel { + fn latest(&self) -> u64 { + self.entries.last().map(|(_, value)| *value).unwrap_or(0) + } + + fn push(&mut self, timepoint: u64, value: u64) -> bool { + if let Some(last) = self.entries.last_mut() { + if timepoint < last.0 { + return false; + } + if timepoint == last.0 { + last.1 = value; + return true; + } + } + self.entries.push((timepoint, value)); + true + } + + fn get_at(&self, timepoint: u64) -> u64 { + self.entries + .iter() + .rev() + .find_map(|(key, value)| (*key <= timepoint).then_some(*value)) + .unwrap_or(0) + } +} + +#[derive(Clone, Debug, Default)] +struct VotingModel { + accounts: BTreeMap, + total_supply: CheckpointModel, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct VotingSnapshot { + latest_votes: Vec<(Principal, u64)>, + past_votes: Vec<(Principal, u64, u64)>, + latest_total: u64, + past_total: Vec<(u64, u64)>, +} + +impl VotingModel { + fn latest_votes(&self, account: Principal) -> u64 { + self.accounts + .get(&account) + .map(CheckpointModel::latest) + .unwrap_or(0) + } + + fn write_votes( + &mut self, + account: Principal, + timepoint: u64, + value: u64, + ) -> bool { + self.accounts + .entry(account) + .or_default() + .push(timepoint, value) + } + + fn apply(&mut self, op: &VotingOp) -> bool { + match *op { + VotingOp::Write { + account, + timepoint, + value, + } => self.write_votes(account, timepoint, value), + VotingOp::Move { + from, + to, + amount, + timepoint, + } => { + if amount == 0 || from == to { + return true; + } + let from_next = if let Some(from) = from { + let current = self.latest_votes(from); + if current < amount { + return false; + } + Some((from, current - amount)) + } else { + None + }; + let to_next = if let Some(to) = to { + let Some(next) = self.latest_votes(to).checked_add(amount) + else { + return false; + }; + Some((to, next)) + } else { + None + }; + let total_next = match (from, to) { + (None, Some(_)) => { + let Some(next) = + self.total_supply.latest().checked_add(amount) + else { + return false; + }; + Some(next) + } + (Some(_), None) => { + let current = self.total_supply.latest(); + if current < amount { + return false; + } + Some(current - amount) + } + _ => None, + }; + + let mut next = self.clone(); + if let Some((account, value)) = from_next { + if !next.write_votes(account, timepoint, value) { + return false; + } + } + if let Some((account, value)) = to_next { + if !next.write_votes(account, timepoint, value) { + return false; + } + } + if let Some(value) = total_next { + if !next.total_supply.push(timepoint, value) { + return false; + } + } + *self = next; + true + } + } + } + + fn snapshot( + &self, + accounts: &[Principal], + timepoints: &[u64], + ) -> VotingSnapshot { + VotingSnapshot { + latest_votes: accounts + .iter() + .copied() + .map(|account| (account, self.latest_votes(account))) + .collect(), + past_votes: accounts + .iter() + .copied() + .flat_map(|account| { + timepoints.iter().copied().map(move |timepoint| { + let value = self + .accounts + .get(&account) + .map(|trace| trace.get_at(timepoint)) + .unwrap_or(0); + (account, timepoint, value) + }) + }) + .collect(), + latest_total: self.total_supply.latest(), + past_total: timepoints + .iter() + .copied() + .map(|timepoint| { + (timepoint, self.total_supply.get_at(timepoint)) + }) + .collect(), + } + } +} + +fn voting_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let account_or_none = + prop_oneof![Just(None), account.clone().prop_map(Some)]; + let amount = amount_strategy(); + let timepoint = prop_oneof![ + 12 => 0u64..=24, + 1 => (0u64..=4).prop_map(|d| u64::MAX - d), + ]; + prop_oneof![ + ( + account_or_none.clone(), + account_or_none, + amount.clone(), + timepoint.clone() + ) + .prop_map(|(from, to, amount, timepoint)| VotingOp::Move { + from, + to, + amount, + timepoint, + }), + (account, timepoint, amount).prop_map(|(account, timepoint, value)| { + VotingOp::Write { + account, + timepoint, + value, + } + },), + ] +} + +fn voting_snapshot( + votes: &VotingUnits, + accounts: &[Principal], + timepoints: &[u64], +) -> VotingSnapshot { + VotingSnapshot { + latest_votes: accounts + .iter() + .copied() + .map(|account| (account, votes.latest_votes(account))) + .collect(), + past_votes: accounts + .iter() + .copied() + .flat_map(|account| { + timepoints.iter().copied().map(move |timepoint| { + (account, timepoint, votes.past_votes(account, timepoint)) + }) + }) + .collect(), + latest_total: votes.latest_total_supply(), + past_total: timepoints + .iter() + .copied() + .map(|timepoint| (timepoint, votes.past_total_supply(timepoint))) + .collect(), + } +} + +fn apply_voting(votes: &mut VotingUnits, op: &VotingOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + VotingOp::Move { + from, + to, + amount, + timepoint, + } => votes.move_units(from, to, amount, timepoint), + VotingOp::Write { + account, + timepoint, + value, + } => votes.write_votes(account, timepoint, value), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum RoyaltyOp { + SetDefault { info: RoyaltyInfo }, + ClearDefault, + SetToken { token_id: u64, info: RoyaltyInfo }, + ClearToken { token_id: u64 }, + Query { token_id: u64, sale_price: u64 }, +} + +#[derive(Clone, Debug, Default)] +struct RoyaltyModel { + default: Option, + tokens: BTreeMap, +} + +type RoyaltyQuoteSnapshot = (u64, u64, Option<(Principal, u64)>); + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RoyaltySnapshot { + default: Option, + royalty_for: Vec<(u64, Option)>, + quotes: Vec, +} + +impl RoyaltyModel { + fn royalty_for(&self, token_id: u64) -> Option { + self.tokens.get(&token_id).copied().or(self.default) + } + + fn quote( + &self, + token_id: u64, + sale_price: u64, + ) -> Option<(Principal, u64)> { + let Some(info) = self.royalty_for(token_id) else { + return Some((principal(0), 0)); + }; + let amount = sale_price.checked_mul(u64::from(info.basis_points))? + / u64::from(MAX_BASIS_POINTS); + Some((info.receiver, amount)) + } + + fn apply(&mut self, op: &RoyaltyOp) -> bool { + match *op { + RoyaltyOp::SetDefault { info } => { + if !valid_royalty(info) { + return false; + } + self.default = Some(info); + true + } + RoyaltyOp::ClearDefault => { + self.default = None; + true + } + RoyaltyOp::SetToken { token_id, info } => { + if !valid_royalty(info) { + return false; + } + self.tokens.insert(token_id, info); + true + } + RoyaltyOp::ClearToken { token_id } => { + self.tokens.remove(&token_id); + true + } + RoyaltyOp::Query { + token_id, + sale_price, + } => self.quote(token_id, sale_price).is_some(), + } + } + + fn snapshot( + &self, + token_ids: &[u64], + sale_prices: &[u64], + ) -> RoyaltySnapshot { + RoyaltySnapshot { + default: self.default, + royalty_for: token_ids + .iter() + .copied() + .map(|token_id| (token_id, self.royalty_for(token_id))) + .collect(), + quotes: token_ids + .iter() + .copied() + .flat_map(|token_id| { + sale_prices.iter().copied().map(move |sale_price| { + (token_id, sale_price, self.quote(token_id, sale_price)) + }) + }) + .collect(), + } + } +} + +fn valid_royalty(info: RoyaltyInfo) -> bool { + !info.receiver.is_zero() && info.basis_points <= MAX_BASIS_POINTS +} + +fn royalty_info_strategy() -> BoxedStrategy { + (principal_strategy(), 0u16..=10_500) + .prop_map(|(receiver, basis_points)| RoyaltyInfo { + receiver, + basis_points, + }) + .boxed() +} + +fn royalty_op_strategy() -> impl Strategy { + let token_id = 0u64..=5; + let sale_price = prop_oneof![ + 12 => 0u64..=100_000, + 2 => (0u64..=16).prop_map(|d| u64::MAX - d), + 1 => Just(u64::MAX), + ]; + let info = royalty_info_strategy(); + prop_oneof![ + info.clone().prop_map(|info| RoyaltyOp::SetDefault { info }), + Just(RoyaltyOp::ClearDefault), + (token_id.clone(), info).prop_map(|(token_id, info)| { + RoyaltyOp::SetToken { token_id, info } + }), + token_id + .clone() + .prop_map(|token_id| RoyaltyOp::ClearToken { token_id }), + (token_id, sale_price).prop_map(|(token_id, sale_price)| { + RoyaltyOp::Query { + token_id, + sale_price, + } + }), + ] +} + +fn royalty_snapshot( + royalties: &RoyaltyRegistry, + token_ids: &[u64], + sale_prices: &[u64], +) -> RoyaltySnapshot { + RoyaltySnapshot { + default: royalties.default_royalty(), + royalty_for: token_ids + .iter() + .copied() + .map(|token_id| (token_id, royalties.royalty_for(token_id))) + .collect(), + quotes: token_ids + .iter() + .copied() + .flat_map(|token_id| { + sale_prices.iter().copied().map(move |sale_price| { + let quote = catch_unwind(AssertUnwindSafe(|| { + royalties.royalty_info(token_id, sale_price) + })) + .ok() + .map(|quote| (quote.receiver, quote.amount)); + (token_id, sale_price, quote) + }) + }) + .collect(), + } +} + +fn apply_royalty(royalties: &mut RoyaltyRegistry, op: &RoyaltyOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + RoyaltyOp::SetDefault { info } => royalties.set_default_royalty(info), + RoyaltyOp::ClearDefault => royalties.clear_default_royalty(), + RoyaltyOp::SetToken { token_id, info } => { + royalties.set_token_royalty(token_id, info); + } + RoyaltyOp::ClearToken { token_id } => { + royalties.clear_token_royalty(token_id) + } + RoyaltyOp::Query { + token_id, + sale_price, + } => { + royalties.royalty_info(token_id, sale_price); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum CapOp { + AssertMint { current_supply: u64, amount: u64 }, + SetCap { current_supply: u64, cap: u64 }, +} + +fn cap_op_strategy() -> impl Strategy { + let amount = amount_strategy(); + prop_oneof![ + (amount.clone(), amount.clone()).prop_map( + |(current_supply, amount)| CapOp::AssertMint { + current_supply, + amount, + }, + ), + (amount.clone(), amount).prop_map(|(current_supply, cap)| { + CapOp::SetCap { + current_supply, + cap, + } + }), + ] +} + +proptest! { + #![proptest_config(standards_proptest_config())] + + #[test] + fn drc20_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc20_op_strategy(), 0..160), + ) { + let accounts = all_principals(); + let mut token = Drc20::new(); + token.init(Init20 { + name: "Property Token".into(), + symbol: "PROP".into(), + decimals: 9, + initial_balances: vec![ + InitBalance { account: principal(1), amount: 100 }, + InitBalance { account: principal(2), amount: 50 }, + ], + }); + let mut model = Drc20Model::initial(); + prop_assert_eq!(drc20_snapshot(&token, &accounts), model.snapshot(&accounts)); + + for op in ops { + let before = drc20_snapshot(&token, &accounts); + let expected = model.apply(&op); + let actual = apply_drc20_token(&mut token, &op); + prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + model.snapshot(&accounts), + "successful operation left an unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + before, + "failed operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn drc721_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc721_op_strategy(), 0..140), + ) { + let accounts = all_principals(); + let token_ids: Vec = (0..=8).collect(); + let mut token = Drc721::new(); + token.init(Init721 { + name: "Property NFT".into(), + symbol: "PNFT".into(), + base_uri: "ipfs://property/".into(), + initial_tokens: vec![ + InitToken { account: principal(1), token_id: 1 }, + InitToken { account: principal(2), token_id: 2 }, + ], + }); + let mut model = Drc721Model::initial(); + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + model.snapshot(&accounts, &token_ids), + ); + + for op in ops { + let before = drc721_snapshot(&token, &accounts, &token_ids); + let expected = model.apply(&op); + let actual = apply_drc721_token(&mut token, &op); + prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + model.snapshot(&accounts, &token_ids), + "successful operation left an unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + before, + "failed operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn authorization_negative_matrix_preserves_nonce(case in auth_case_strategy()) { + let (ok, nonce) = authorization_probe(case); + let expected_ok = matches!(case, AuthCase::Good); + prop_assert_eq!(ok, expected_ok, "authorization case diverged: {:?}", case); + prop_assert_eq!( + nonce, + if expected_ok { 1 } else { 0 }, + "authorization case moved nonce incorrectly: {:?}", + case, + ); + } + + #[test] + fn phoenix_authorization_negative_matrix_preserves_nonce_and_replay( + case in phoenix_auth_case_strategy(), + ) { + let (ok, nonce, replay_used) = phoenix_authorization_probe(case); + let expected_ok = matches!(case, PhoenixAuthCase::Good); + prop_assert_eq!(ok, expected_ok, "Phoenix authorization case diverged: {:?}", case); + prop_assert_eq!( + nonce, + if expected_ok { 1 } else { 0 }, + "Phoenix authorization case moved nonce incorrectly: {:?}", + case, + ); + prop_assert_eq!( + replay_used, + matches!(case, PhoenixAuthCase::Good | PhoenixAuthCase::ReplayKeyUsed), + "Phoenix authorization case moved replay state incorrectly: {:?}", + case, + ); + } + + #[test] + fn owner_set_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(owner_set_op_strategy(), 0..120), + ) { + let mut owners = OwnerSet::new(); + owners.init([principal(1), principal(2)]); + let mut model = BTreeSet::new(); + model.insert(principal(1)); + model.insert(principal(2)); + prop_assert_eq!(owners.owners(), model.iter().copied().collect::>()); + + for op in ops { + let before = owners.owners(); + let expected = apply_owner_set_model(&mut model, &op); + let actual = apply_owner_set(&mut owners, &op); + prop_assert_eq!(actual, expected, "owner-set operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + owners.owners(), + model.iter().copied().collect::>(), + "successful owner-set operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + owners.owners(), + before, + "failed owner-set operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn owner_set_init_rejects_invalid_inputs_without_partial_state( + initial_owners in prop::collection::vec(principal_strategy(), 0..16), + ) { + let mut expected = BTreeSet::new(); + let mut has_zero = false; + for owner in &initial_owners { + has_zero |= owner.is_zero(); + expected.insert(*owner); + } + let expected_ok = !has_zero && !expected.is_empty(); + + let mut owners = OwnerSet::new(); + let actual_ok = catch_unwind(AssertUnwindSafe(|| { + owners.init(initial_owners.clone()); + })) + .is_ok(); + + prop_assert_eq!(actual_ok, expected_ok); + if expected_ok { + prop_assert_eq!( + owners.owners(), + expected.iter().copied().collect::>(), + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + owners.init([principal(3)]); + })) + .is_err(), + "successful initialization allowed a second init", + ); + } else { + prop_assert!( + owners.is_empty(), + "failed initialization left partial owner state", + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + owners.init([principal(1)]); + })) + .is_ok(), + "failed initialization poisoned the owner set", + ); + } + } + + #[test] + fn ownable2_step_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(ownable2_step_op_strategy(), 0..120), + ) { + let owner = principal(1); + let mut ownable = Ownable2Step::new(); + ownable.init(owner); + let mut model = Ownable2StepModel::new(owner); + prop_assert_eq!(ownable2_step_snapshot(&ownable), model.snapshot()); + + for op in ops { + let before = ownable2_step_snapshot(&ownable); + let expected = model.apply(&op); + let actual = apply_ownable2_step(&mut ownable, &op); + prop_assert_eq!(actual, expected, "ownable2-step operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + ownable2_step_snapshot(&ownable), + model.snapshot(), + "successful ownable2-step operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + ownable2_step_snapshot(&ownable), + before, + "failed ownable2-step operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn access_control_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(access_op_strategy(), 0..140), + ) { + let roles = [DEFAULT_ADMIN_ROLE, role(1), role(2), role(3)]; + let accounts = all_principals(); + let admin = principal(1); + let mut access = AccessControl::new(); + access.init_admin(admin); + let mut model = AccessModel::new(admin); + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + model.snapshot(&roles, &accounts), + ); + + for op in ops { + let before = access_snapshot(&access, &roles, &accounts); + let expected = model.apply(&op); + let actual = apply_access(&mut access, &op); + prop_assert_eq!(actual, expected, "access-control operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + model.snapshot(&roles, &accounts), + "successful access-control operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + before, + "failed access-control operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn multisig_controller_two_of_three_threshold_and_rejections_are_consistent( + proposer in 0usize..3, + second in 0usize..3, + ttl in 2u64..64, + tombstone_ttl in 1u64..64, + now in 0u64..64, + id_seed in any::(), + contract_seed in 1u8..=250, + arg_seed in any::(), + salt_seed in any::(), + ) { + let owners = [principal(1), principal(2), principal(3)]; + let outsider = principal(4); + let id = [id_seed; 32]; + let target = multisig_target(contract_seed, arg_seed, salt_seed); + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: owners.to_vec(), + threshold: 2, + proposal_ttl: ttl, + tombstone_ttl, + }); + + let before = multisig_snapshot(&controller, id); + let outsider_propose = catch_unwind(AssertUnwindSafe(|| { + controller.propose(id, target.clone(), outsider, now); + })) + .is_ok(); + prop_assert!(!outsider_propose, "non-owner proposed an operation"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed outsider proposal mutated controller state", + ); + + let proposed = controller.propose(id, target.clone(), owners[proposer], now); + prop_assert_eq!(proposed.status, MultisigControllerStatus::Proposed); + prop_assert_eq!(proposed.confirmations, 1); + prop_assert_eq!(proposed.threshold, 2); + prop_assert!(proposed.ready_operation.is_none()); + let pending = controller.proposal(id).expect("one-of-three stays pending"); + prop_assert_eq!(&pending.target, &target); + prop_assert_eq!(pending.confirmations, vec![owners[proposer]]); + prop_assert_eq!(pending.deadline, now + ttl); + + let before = multisig_snapshot(&controller, id); + let outsider_confirm = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, outsider, now); + })) + .is_ok(); + prop_assert!(!outsider_confirm, "non-owner confirmed an operation"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed outsider confirmation mutated controller state", + ); + + let before = multisig_snapshot(&controller, id); + let duplicate_confirm = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, owners[proposer], now); + })) + .is_ok(); + prop_assert!(!duplicate_confirm, "duplicate owner confirmation succeeded"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed duplicate confirmation mutated controller state", + ); + + let mut expired = controller.clone(); + let expired_confirm = catch_unwind(AssertUnwindSafe(|| { + expired.confirm(id, owners[(proposer + 1) % owners.len()], now + ttl + 1); + })) + .is_ok(); + prop_assert!(!expired_confirm, "expired proposal was confirmed"); + prop_assert!( + expired.proposal(id).is_none(), + "expired proposal was not pruned", + ); + prop_assert!( + expired.tombstone_expiry(id).is_none(), + "expired proposal created a tombstone", + ); + + if second == proposer { + let before = multisig_snapshot(&controller, id); + let duplicate_second = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, owners[second], now); + })) + .is_ok(); + prop_assert!(!duplicate_second, "duplicate generated signer reached quorum"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed generated duplicate mutated controller state", + ); + return Ok(()); + } + + let ready = controller.confirm(id, owners[second], now); + prop_assert_eq!(ready.status, MultisigControllerStatus::Ready); + prop_assert_eq!(ready.confirmations, 2); + prop_assert_eq!(ready.threshold, 2); + let ready_operation = ready.ready_operation.expect("quorum operation"); + prop_assert_eq!(&ready_operation.target, &target); + prop_assert_eq!( + ready_operation.confirmations, + vec![owners[proposer], owners[second]], + ); + prop_assert!(controller.proposal(id).is_none()); + prop_assert_eq!(controller.tombstone_expiry(id), Some(now + tombstone_ttl)); + + let before = multisig_snapshot(&controller, id); + let tombstoned_reproposal = catch_unwind(AssertUnwindSafe(|| { + controller.propose(id, target.clone(), owners[(proposer + 2) % owners.len()], now + tombstone_ttl); + })) + .is_ok(); + prop_assert!( + !tombstoned_reproposal, + "operation id was re-used before tombstone expiry", + ); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed tombstoned re-proposal mutated controller state", + ); + + let reproposed = controller.propose( + id, + target, + owners[(proposer + 2) % owners.len()], + now + tombstone_ttl + 1, + ); + prop_assert_eq!(reproposed.status, MultisigControllerStatus::Proposed); + prop_assert_eq!(reproposed.confirmations, 1); + prop_assert!(controller.proposal(id).is_some()); + } + + #[test] + fn timelock_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(timelock_op_strategy(), 0..140), + ) { + let ids: Vec<[u8; 32]> = (0u8..=5).map(|i| [i; 32]).collect(); + let mut timelock = Timelock::new(5); + let mut model = TimelockModel::new(5); + prop_assert_eq!(timelock_snapshot(&timelock, &ids), model.snapshot(&ids)); + + for op in ops { + let before = timelock_snapshot(&timelock, &ids); + let expected = model.apply(&op); + let actual = apply_timelock(&mut timelock, &op); + prop_assert_eq!(actual, expected, "timelock operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + timelock_snapshot(&timelock, &ids), + model.snapshot(&ids), + "successful timelock operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + timelock_snapshot(&timelock, &ids), + before, + "failed timelock operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn timelock_controller_rejects_bad_min_delay_payloads_atomically( + payload in bad_min_delay_payload_strategy(), + now in 0u64..=64, + ) { + let self_principal = principal(2); + let admin = principal(1); + let mut controller = TimelockController::new(self_principal, admin, 5); + let id = [88u8; 32]; + prop_assert!(controller.has_role(PROPOSER_ROLE, admin)); + prop_assert!(controller.has_role(EXECUTOR_ROLE, admin)); + + let ready_at = controller.schedule(admin, id, now, payload); + let before = controller.get(id).cloned(); + let actual = catch_unwind(AssertUnwindSafe(|| { + controller.execute_min_delay_change(admin, id, ready_at); + })) + .is_ok(); + + prop_assert!(!actual, "bad min-delay payload unexpectedly executed"); + prop_assert_eq!(controller.timelock().min_delay(), 5); + prop_assert_eq!( + controller.get(id).cloned(), + before, + "bad min-delay payload mutated scheduled operation state", + ); + } + + #[test] + fn timelock_controller_min_delay_change_flow_preserves_delay_until_ready( + new_delay in any::(), + now in 0u64..=64, + ) { + let self_principal = principal(2); + let admin = principal(1); + let mut controller = TimelockController::new(self_principal, admin, 5); + let id = [89u8; 32]; + let ready_at = controller.schedule_min_delay_change( + admin, + id, + now, + new_delay, + ); + let before = controller.get(id).cloned(); + + let early = catch_unwind(AssertUnwindSafe(|| { + controller.execute_min_delay_change(admin, id, ready_at - 1); + })) + .is_ok(); + prop_assert!(!early, "min-delay change executed before ready_at"); + prop_assert_eq!(controller.timelock().min_delay(), 5); + prop_assert_eq!(controller.get(id).cloned(), before); + + let applied = controller.execute_min_delay_change(admin, id, ready_at); + prop_assert_eq!(applied, new_delay); + prop_assert_eq!(controller.timelock().min_delay(), new_delay); + prop_assert!( + controller.get(id).map(|operation| operation.done).unwrap_or(false), + "executed min-delay change was not marked done", + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + controller.set_min_delay(admin, 5); + })) + .is_err(), + "admin changed delay directly without timelock self-call", + ); + } + + #[test] + fn upgrade_admin_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(upgrade_op_strategy(), 0..120), + ) { + let admin = principal(1); + let implementation = contract_id(1); + let mut upgrade = UpgradeAdmin::new(admin, implementation, 5, 10); + let mut model = UpgradeModel::new(admin, implementation); + prop_assert_eq!(upgrade_snapshot(&upgrade), model.snapshot()); + + for op in ops { + let before = upgrade_snapshot(&upgrade); + let expected = model.apply(&op); + let actual = apply_upgrade(&mut upgrade, &op); + prop_assert_eq!(actual, expected, "upgrade operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + upgrade_snapshot(&upgrade), + model.snapshot(), + "successful upgrade operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + upgrade_snapshot(&upgrade), + before, + "failed upgrade operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn nonce_and_replay_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(nonce_replay_op_strategy(), 0..180), + ) { + let mut nonces = NonceManager::new(); + let mut replays = ReplayGuard::new(); + let mut model = NonceReplayModel::default(); + prop_assert_eq!(nonce_replay_snapshot(&nonces, &replays), model.snapshot()); + + for op in ops { + let before = nonce_replay_snapshot(&nonces, &replays); + let expected = model.apply(&op); + let actual = apply_nonce_replay(&mut nonces, &mut replays, &op); + prop_assert_eq!(actual, expected, "nonce/replay operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + nonce_replay_snapshot(&nonces, &replays), + model.snapshot(), + "successful nonce/replay operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + nonce_replay_snapshot(&nonces, &replays), + before, + "failed nonce/replay operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn voting_units_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(voting_op_strategy(), 0..160), + ) { + let accounts = all_principals(); + let timepoints = [0, 1, 2, 3, 4, 8, 16, 24, u64::MAX]; + let mut votes = VotingUnits::new(); + let mut model = VotingModel::default(); + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + model.snapshot(&accounts, &timepoints), + ); + + for op in ops { + let before = voting_snapshot(&votes, &accounts, &timepoints); + let expected = model.apply(&op); + let actual = apply_voting(&mut votes, &op); + prop_assert_eq!(actual, expected, "voting operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + model.snapshot(&accounts, &timepoints), + "successful voting operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + before, + "failed voting operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn royalty_registry_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(royalty_op_strategy(), 0..140), + ) { + let token_ids = [0, 1, 2, 3, 4, 5]; + let sale_prices = [0, 1, 10_000, 100_000, u64::MAX]; + let mut royalties = RoyaltyRegistry::new(); + let mut model = RoyaltyModel::default(); + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + model.snapshot(&token_ids, &sale_prices), + ); + + for op in ops { + let before = royalty_snapshot(&royalties, &token_ids, &sale_prices); + let expected = model.apply(&op); + let actual = apply_royalty(&mut royalties, &op); + prop_assert_eq!(actual, expected, "royalty operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + model.snapshot(&token_ids, &sale_prices), + "successful royalty operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + before, + "failed royalty operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn supply_cap_rejects_overflow_and_cap_reduction( + initial_cap in amount_strategy(), + ops in prop::collection::vec(cap_op_strategy(), 0..120), + ) { + let mut cap = SupplyCap::new(initial_cap); + let mut model_cap = initial_cap; + prop_assert_eq!(cap.cap(), model_cap); + + for op in ops { + let before = cap.cap(); + let expected = match op { + CapOp::AssertMint { current_supply, amount } => { + current_supply + .checked_add(amount) + .map(|next| next <= model_cap) + .unwrap_or(false) + } + CapOp::SetCap { current_supply, cap } => { + if cap < current_supply { + false + } else { + model_cap = cap; + true + } + } + }; + let actual = catch_unwind(AssertUnwindSafe(|| match op { + CapOp::AssertMint { current_supply, amount } => { + cap.assert_mint(current_supply, amount) + } + CapOp::SetCap { current_supply, cap: next_cap } => { + cap.set_cap(current_supply, next_cap) + } + })) + .is_ok(); + prop_assert_eq!(actual, expected, "supply-cap operation diverged: {:?}", op); + if expected { + prop_assert_eq!(cap.cap(), model_cap); + } else { + prop_assert_eq!( + cap.cap(), + before, + "failed supply-cap operation mutated state: {:?}", + op, + ); + } + } + } +} diff --git a/standards/examples/authorization_counter/Cargo.toml b/standards/examples/authorization_counter/Cargo.toml new file mode 100644 index 00000000..628f068d --- /dev/null +++ b/standards/examples/authorization_counter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "authorization-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/authorization_counter/Makefile b/standards/examples/authorization_counter/Makefile new file mode 100644 index 00000000..1cc96d25 --- /dev/null +++ b/standards/examples/authorization_counter/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p authorization-counter + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p authorization-counter + +clippy: + @cargo clippy -p authorization-counter -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs new file mode 100644 index 00000000..c20eda20 --- /dev/null +++ b/standards/examples/authorization_counter/src/lib.rs @@ -0,0 +1,147 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Example counter controlled by replay-protected Dusk authorizations. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as authorization_counter_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::{ + MoonlightAuthorization, PhoenixSignatureAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use rkyv::{Archive, Deserialize, Serialize}; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValueByMoonlight { + pub authorization: MoonlightAuthorization, + pub amount: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValueByPhoenix { + pub authorization: PhoenixSignatureAuthorization, + pub amount: u64, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + pub principal: Principal, + pub domain: NonceDomain, +} + +#[dusk_forge::contract] +mod authorization_counter { + use alloc::vec::Vec; + + use authorization_counter_types::{ + NonceQuery, SetValueByMoonlight, SetValueByPhoenix, + }; + use dusk_contract_standards::auth::{ActionEnvelope, AuthorizationManager}; + use dusk_contract_standards::core::{NonceDomain, Principal}; + use dusk_core::abi; + + const SET_VALUE_DOMAIN: NonceDomain = [3u8; 32]; + const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; + + pub struct AuthorizationCounter { + authorizations: AuthorizationManager, + value: u64, + last_authorizer: Option, + initialized: bool, + } + + impl AuthorizationCounter { + pub const fn new() -> Self { + Self { + authorizations: AuthorizationManager::new(), + value: 0, + last_authorizer: None, + initialized: false, + } + } + pub fn init(&mut self) { + if self.initialized { + panic!("AuthorizationCounter: already initialized"); + } + self.initialized = true; + } + + pub fn value(&self) -> u64 { + self.value + } + + pub const fn last_authorizer(&self) -> Option { + self.last_authorizer + } + + pub fn nonce(&self, query: NonceQuery) -> u64 { + self.authorizations.nonce(query.principal, query.domain) + } + pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { + let envelope = self.action_envelope(args.amount); + let principal = self.authorizations.authorize_moonlight_action( + &args.authorization, + envelope, + now(), + ); + self.set_value(principal, args.amount); + } + pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { + let envelope = self.action_envelope(args.amount); + let principal = self.authorizations.authorize_phoenix_action( + &args.authorization, + envelope, + now(), + ); + self.set_value(principal, args.amount); + } + + fn action_envelope(&self, amount: u64) -> ActionEnvelope { + if !self.initialized { + panic!("AuthorizationCounter: not initialized"); + } + ActionEnvelope::new( + abi::chain_id(), + abi::self_id(), + SET_VALUE_DOMAIN, + SET_VALUE_ACTION, + amount_hash(amount), + ) + } + + fn set_value(&mut self, principal: Principal, amount: u64) { + self.value = amount; + self.last_authorizer = Some(principal); + } + } + + impl Default for AuthorizationCounter { + fn default() -> Self { + Self::new() + } + } + + fn amount_hash(amount: u64) -> [u8; 32] { + abi::keccak256(Vec::from(amount.to_be_bytes())) + } + + fn now() -> u64 { + abi::block_height() + } +} diff --git a/standards/examples/drc20_roles_pausable/Cargo.toml b/standards/examples/drc20_roles_pausable/Cargo.toml new file mode 100644 index 00000000..bb9a6488 --- /dev/null +++ b/standards/examples/drc20_roles_pausable/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "drc20-roles-pausable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/drc20_roles_pausable/Makefile b/standards/examples/drc20_roles_pausable/Makefile new file mode 100644 index 00000000..3357eb79 --- /dev/null +++ b/standards/examples/drc20_roles_pausable/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p drc20-roles-pausable + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p drc20-roles-pausable + +clippy: + @cargo clippy -p drc20-roles-pausable -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs new file mode 100644 index 00000000..e30ef9b8 --- /dev/null +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -0,0 +1,557 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Example DRC20 composed from reusable Dusk contract standards modules. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as drc20_roles_pausable_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::access::Role; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::token::drc20::Init as TokenInit; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const TOKEN_ADMIN_DOMAIN: NonceDomain = [11u8; 32]; +pub const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +pub const MINT_ACTION: [u8; 32] = [13u8; 32]; +pub const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +pub const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +pub const GRANT_ROLE_ACTION: [u8; 32] = [16u8; 32]; +pub const REVOKE_ROLE_ACTION: [u8; 32] = [17u8; 32]; +pub const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub admin: Principal, + pub token: TokenInit, + /// Optional max supply. A value of zero means uncapped. + pub cap: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MintCall { + pub to: Principal, + pub amount: u64, + pub authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleQuery { + pub role: Role, + pub account: Principal, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleCall { + pub role: Role, + pub account: Principal, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct VotesQuery { + pub account: Principal, + pub timepoint: u64, +} + +#[dusk_forge::contract(events = [ + Drc20Transfer, + Drc20Approval, + Paused, + Unpaused, + RoleGranted, + RoleRevoked, +])] +mod drc20_roles_pausable { + use alloc::string::String; + use alloc::vec::Vec; + + use drc20_roles_pausable_types::{ + AdminCall, Init, MintCall, RoleCall, RoleQuery, VotesQuery, + GRANT_ROLE_ACTION, MINT_ACTION, PAUSE_ACTION, REVOKE_ROLE_ACTION, + SIGNED_APPROVE_ACTION, SIGNED_APPROVE_DOMAIN, TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + }; + use dusk_contract_standards::access::events::{ + Paused, RoleGranted, RoleRevoked, Unpaused, PAUSED_TOPIC, + ROLE_GRANTED_TOPIC, ROLE_REVOKED_TOPIC, UNPAUSED_TOPIC, + }; + use dusk_contract_standards::access::{AccessControl, Pausable, Role}; + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::security::ReentrancyGuard; + use dusk_contract_standards::token::drc20::events::{ + Approval as Drc20Approval, Transfer as Drc20Transfer, APPROVAL_TOPIC, + TRANSFER_TOPIC, + }; + use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf, DecreaseAllowanceCall, Drc20, + IncreaseAllowanceCall, SignedApproveCall, SupplyCap, TransferCall, + TransferFromCall, VotingUnits, + }; + use dusk_core::abi; + + const MINTER_ROLE: Role = [1u8; 32]; + const PAUSER_ROLE: Role = [2u8; 32]; + + pub struct Drc20RolesPausable { + token: Drc20, + access: AccessControl, + authorizations: AuthorizationManager, + pausable: Pausable, + guard: ReentrancyGuard, + cap: Option, + votes: VotingUnits, + } + + impl Drc20RolesPausable { + pub const fn new() -> Self { + Self { + token: Drc20::new(), + access: AccessControl::new(), + authorizations: AuthorizationManager::new(), + pausable: Pausable::new(), + guard: ReentrancyGuard::new(), + cap: None, + votes: VotingUnits::new(), + } + } + pub fn init(&mut self, args: Init) { + if args.admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut initial_supply = 0u64; + for balance in &args.token.initial_balances { + if balance.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + initial_supply = initial_supply + .checked_add(balance.amount) + .expect(error::OVERFLOW); + } + if args.cap != 0 { + SupplyCap::new(args.cap).assert_mint(0, initial_supply); + } + + let initial_balances = args.token.initial_balances.clone(); + let events = self.token.init(args.token); + self.access.init_admin(args.admin); + self.access.grant_role(args.admin, MINTER_ROLE, args.admin); + self.access.grant_role(args.admin, PAUSER_ROLE, args.admin); + for event in events { + Self::emit_transfer(event); + } + if args.cap != 0 { + let cap = SupplyCap::new(args.cap); + self.cap = Some(cap); + } + for balance in initial_balances { + self.votes.move_units( + None, + Some(balance.account), + balance.amount, + now(), + ); + } + } + + pub fn name(&self) -> String { + self.token.name() + } + + pub fn symbol(&self) -> String { + self.token.symbol() + } + + pub fn decimals(&self) -> u8 { + self.token.decimals() + } + + pub fn total_supply(&self) -> u64 { + self.token.total_supply() + } + + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.token.balance_of(args) + } + + pub fn allowance(&self, args: Allowance) -> u64 { + self.token.allowance(args) + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn cap(&self) -> u64 { + self.cap.map(|cap| cap.cap()).unwrap_or(0) + } + + pub fn remaining_mintable(&self) -> u64 { + self.cap + .map(|cap| cap.remaining(self.token.total_supply())) + .unwrap_or(u64::MAX) + } + + pub fn latest_votes(&self, account: Principal) -> u64 { + self.votes.latest_votes(account) + } + + pub fn past_votes(&self, args: VotesQuery) -> u64 { + self.votes.past_votes(args.account, args.timepoint) + } + + pub fn latest_total_votes(&self) -> u64 { + self.votes.latest_total_supply() + } + + pub fn past_total_supply(&self, timepoint: u64) -> u64 { + self.votes.past_total_supply(timepoint) + } + + pub fn has_role(&self, args: RoleQuery) -> bool { + self.access.has_role(args.role, args.account) + } + pub fn transfer(&mut self, args: TransferCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + let votes = &mut self.votes; + guard.run(|| { + let event = token.transfer(caller, args); + votes.move_units( + Some(event.from), + Some(event.to), + event.amount, + now(), + ); + event + }) + }; + Self::emit_transfer(event); + } + pub fn approve(&mut self, args: ApproveCall) { + let caller = caller(); + let event = self.token.approve(caller, args); + Self::emit_approval(event); + } + pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::for_current_chain( + abi::self_id(), + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + approve_payload_hash(args.owner, args.spender, args.amount), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.approve_for( + args.owner, + ApproveCall { + spender: args.spender, + amount: args.amount, + }, + ); + Self::emit_approval(event); + } + pub fn increase_allowance(&mut self, args: IncreaseAllowanceCall) { + let caller = caller(); + let event = self.token.increase_allowance(caller, args); + Self::emit_approval(event); + } + pub fn decrease_allowance(&mut self, args: DecreaseAllowanceCall) { + let caller = caller(); + let event = self.token.decrease_allowance(caller, args); + Self::emit_approval(event); + } + pub fn transfer_from(&mut self, args: TransferFromCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + let votes = &mut self.votes; + guard.run(|| { + let event = token.transfer_from(caller, args); + votes.move_units( + Some(event.from), + Some(event.to), + event.amount, + now(), + ); + event + }) + }; + Self::emit_transfer(event); + } + pub fn mint(&mut self, args: MintCall) { + self.pausable.assert_not_paused(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if let Some(cap) = self.cap { + cap.assert_mint(self.token.total_supply(), args.amount); + } + self.authorize_role_action( + MINTER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + mint_payload_hash(args.to, args.amount), + ), + ); + let event = self.token.mint(args.to, args.amount); + self.votes + .move_units(None, Some(event.to), event.amount, now()); + Self::emit_transfer(event); + } + pub fn burn(&mut self, amount: u64) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = self.token.burn(caller, amount); + self.votes + .move_units(Some(event.from), None, event.amount, now()); + Self::emit_transfer(event); + } + pub fn pause(&mut self, args: AdminCall) { + self.pausable.assert_not_paused(); + let caller = self.authorize_role_action( + PAUSER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + empty_payload_hash(b"drc20.pause"), + ), + ); + self.pausable.pause(); + Self::emit_paused(caller); + } + pub fn unpause(&mut self, args: AdminCall) { + self.pausable.assert_paused(); + let caller = self.authorize_role_action( + PAUSER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + empty_payload_hash(b"drc20.unpause"), + ), + ); + self.pausable.unpause(); + Self::emit_unpaused(caller); + } + + pub fn paused(&self) -> bool { + self.pausable.paused() + } + pub fn grant_role(&mut self, args: RoleCall) { + if args.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let admin = self.access.get_role_admin(args.role); + let caller = self.authorize_role_action( + admin, + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + GRANT_ROLE_ACTION, + role_payload_hash(args.role, args.account), + ), + ); + let had_role = self.access.has_role(args.role, args.account); + self.access.grant_role(caller, args.role, args.account); + if !had_role { + Self::emit_role_granted(args.role, args.account, caller); + } + } + pub fn revoke_role(&mut self, args: RoleCall) { + let admin = self.access.get_role_admin(args.role); + let caller = self.authorize_role_action( + admin, + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + REVOKE_ROLE_ACTION, + role_payload_hash(args.role, args.account), + ), + ); + let had_role = self.access.has_role(args.role, args.account); + self.access.revoke_role(caller, args.role, args.account); + if had_role { + Self::emit_role_revoked(args.role, args.account, caller); + } + } + + fn authorize_role_action( + &mut self, + role: Role, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.access.authorize_role_action( + role, + &mut self.authorizations, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_transfer(event: Drc20Transfer) { + abi::emit( + TRANSFER_TOPIC, + Drc20Transfer { + from: event.from, + to: event.to, + amount: event.amount, + }, + ); + } + + fn emit_approval(event: Drc20Approval) { + abi::emit( + APPROVAL_TOPIC, + Drc20Approval { + owner: event.owner, + spender: event.spender, + amount: event.amount, + }, + ); + } + + fn emit_paused(account: Principal) { + abi::emit(PAUSED_TOPIC, Paused { account }); + } + + fn emit_unpaused(account: Principal) { + abi::emit(UNPAUSED_TOPIC, Unpaused { account }); + } + + fn emit_role_granted( + role: Role, + account: Principal, + sender: Principal, + ) { + abi::emit( + ROLE_GRANTED_TOPIC, + RoleGranted { + role, + account, + sender, + }, + ); + } + + fn emit_role_revoked( + role: Role, + account: Principal, + sender: Principal, + ) { + abi::emit( + ROLE_REVOKED_TOPIC, + RoleRevoked { + role, + account, + sender, + }, + ); + } + } + + impl Default for Drc20RolesPausable { + fn default() -> Self { + Self::new() + } + } + + fn caller() -> Principal { + CallContext::current().require_principal(error::UNAUTHORIZED) + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + abi::keccak256(bytes) + } + + fn approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + abi::keccak256(bytes) + } + + fn role_payload_hash(role: Role, account: Principal) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.role"[..]); + bytes.extend_from_slice(&role); + push_principal(&mut bytes, account); + abi::keccak256(bytes) + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} diff --git a/standards/examples/drc721_collection/Cargo.toml b/standards/examples/drc721_collection/Cargo.toml new file mode 100644 index 00000000..3cf58430 --- /dev/null +++ b/standards/examples/drc721_collection/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "drc721-collection" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/drc721_collection/Makefile b/standards/examples/drc721_collection/Makefile new file mode 100644 index 00000000..02fc3d66 --- /dev/null +++ b/standards/examples/drc721_collection/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p drc721-collection + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p drc721-collection + +clippy: + @cargo clippy -p drc721-collection -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs new file mode 100644 index 00000000..bc4ccbac --- /dev/null +++ b/standards/examples/drc721_collection/src/lib.rs @@ -0,0 +1,614 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Example DRC721 composed from reusable Dusk contract standards modules. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as drc721_collection_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::token::drc721::{Init as TokenInit, RoyaltyInfo}; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; +pub const MINT_ACTION: [u8; 32] = [22u8; 32]; +pub const PAUSE_ACTION: [u8; 32] = [23u8; 32]; +pub const UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +pub const SET_DEFAULT_ROYALTY_ACTION: [u8; 32] = [25u8; 32]; +pub const CLEAR_DEFAULT_ROYALTY_ACTION: [u8; 32] = [26u8; 32]; +pub const SET_TOKEN_ROYALTY_ACTION: [u8; 32] = [27u8; 32]; +pub const CLEAR_TOKEN_ROYALTY_ACTION: [u8; 32] = [28u8; 32]; +pub const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +pub const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +pub const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub owner: Principal, + pub token: TokenInit, + pub default_royalty: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MintCall { + pub to: Principal, + pub token_id: u64, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetDefaultRoyaltyCall { + pub info: RoyaltyInfo, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetTokenRoyaltyCall { + pub token_id: u64, + pub info: RoyaltyInfo, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ClearTokenRoyaltyCall { + pub token_id: u64, + pub authorization: Option, +} + +#[dusk_forge::contract(events = [ + Drc721Transfer, + Drc721Approval, + Drc721ApprovalForAll, + Paused, + Unpaused, + DefaultRoyaltySet, + DefaultRoyaltyCleared, + TokenRoyaltySet, + TokenRoyaltyCleared, +])] +mod drc721_collection { + use alloc::collections::BTreeSet; + use alloc::string::String; + use alloc::vec::Vec; + + use drc721_collection_types::{ + AdminCall, ClearTokenRoyaltyCall, Init, MintCall, + SetDefaultRoyaltyCall, SetTokenRoyaltyCall, + CLEAR_DEFAULT_ROYALTY_ACTION, CLEAR_TOKEN_ROYALTY_ACTION, MINT_ACTION, + NFT_ADMIN_DOMAIN, NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + NFT_SIGNED_APPROVE_ACTION, NFT_SIGNED_APPROVE_DOMAIN, PAUSE_ACTION, + SET_DEFAULT_ROYALTY_ACTION, SET_TOKEN_ROYALTY_ACTION, UNPAUSE_ACTION, + }; + use dusk_contract_standards::access::events::{ + Paused, Unpaused, PAUSED_TOPIC, UNPAUSED_TOPIC, + }; + use dusk_contract_standards::access::{Ownable, Pausable}; + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::security::ReentrancyGuard; + use dusk_contract_standards::token::drc721::events::{ + Approval as Drc721Approval, ApprovalForAll as Drc721ApprovalForAll, + DefaultRoyaltyCleared, DefaultRoyaltySet, TokenRoyaltyCleared, + TokenRoyaltySet, Transfer as Drc721Transfer, APPROVAL_FOR_ALL_TOPIC, + APPROVAL_TOPIC, DEFAULT_ROYALTY_CLEARED_TOPIC, + DEFAULT_ROYALTY_SET_TOPIC, TOKEN_ROYALTY_CLEARED_TOPIC, + TOKEN_ROYALTY_SET_TOPIC, TRANSFER_TOPIC, + }; + use dusk_contract_standards::token::drc721::{ + ApproveCall, BalanceOf, Drc721, GetApproved, IsApprovedForAll, OwnerOf, + RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, + SetApprovalForAllCall, SignedApproveCall, SignedSetApprovalForAllCall, + TokenByIndex, TokenOfOwnerByIndex, TokenUri, TokensOf, + TransferFromCall, MAX_BASIS_POINTS, + }; + use dusk_core::abi; + + pub struct Drc721Collection { + token: Drc721, + ownable: Ownable, + authorizations: AuthorizationManager, + pausable: Pausable, + guard: ReentrancyGuard, + royalties: RoyaltyRegistry, + } + + impl Drc721Collection { + pub const fn new() -> Self { + Self { + token: Drc721::new(), + ownable: Ownable::new(), + authorizations: AuthorizationManager::new(), + pausable: Pausable::new(), + guard: ReentrancyGuard::new(), + royalties: RoyaltyRegistry::new(), + } + } + pub fn init(&mut self, args: Init) { + if args.owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut token_ids = BTreeSet::new(); + for token in &args.token.initial_tokens { + if token.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !token_ids.insert(token.token_id) { + panic!("DRC721: token already minted"); + } + } + let mut royalties = RoyaltyRegistry::new(); + if let Some(info) = args.default_royalty { + royalties.set_default_royalty(info); + } + + self.ownable.init(args.owner); + for event in self.token.init(args.token) { + Self::emit_transfer(event); + } + self.royalties = royalties; + } + + pub fn name(&self) -> String { + self.token.name() + } + + pub fn symbol(&self) -> String { + self.token.symbol() + } + + pub fn base_uri(&self) -> String { + self.token.base_uri() + } + + pub fn token_uri(&self, args: TokenUri) -> String { + self.token.token_uri(args) + } + + pub fn token_by_index(&self, args: TokenByIndex) -> u64 { + self.token.token_by_index(args) + } + + pub fn token_of_owner_by_index( + &self, + args: TokenOfOwnerByIndex, + ) -> u64 { + self.token.token_of_owner_by_index(args) + } + + pub fn tokens_of(&self, args: TokensOf) -> alloc::vec::Vec { + self.token.tokens_of(args) + } + + pub fn total_supply(&self) -> u64 { + self.token.total_supply() + } + + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.token.balance_of(args) + } + + pub fn owner_of(&self, args: OwnerOf) -> Principal { + self.token.owner_of(args) + } + + pub fn get_approved(&self, args: GetApproved) -> Principal { + self.token.get_approved(args) + } + + pub fn is_approved_for_all(&self, args: IsApprovedForAll) -> bool { + self.token.is_approved_for_all(args) + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn royalty_info(&self, args: RoyaltyQuery) -> RoyaltyQuote { + self.royalties.royalty_info(args.token_id, args.sale_price) + } + pub fn approve(&mut self, args: ApproveCall) { + let event = self.token.approve(caller(), args); + Self::emit_approval(event); + } + pub fn set_approval_for_all(&mut self, args: SetApprovalForAllCall) { + let event = self.token.set_approval_for_all(caller(), args); + Self::emit_approval_for_all(event); + } + pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { + let owner = self.token.owner_of(OwnerOf { + token_id: args.token_id, + }); + if args.owner != owner || args.approved == owner { + panic!("{}", error::UNAUTHORIZED); + } + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + approve_payload_hash( + args.owner, + args.approved, + args.token_id, + ), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.approve( + args.owner, + ApproveCall { + approved: args.approved, + token_id: args.token_id, + }, + ); + Self::emit_approval(event); + } + pub fn set_approval_for_all_by_authorization( + &mut self, + args: SignedSetApprovalForAllCall, + ) { + if args.operator.is_zero() || args.operator == args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + approval_for_all_payload_hash( + args.owner, + args.operator, + args.approved, + ), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.set_approval_for_all( + args.owner, + SetApprovalForAllCall { + operator: args.operator, + approved: args.approved, + }, + ); + Self::emit_approval_for_all(event); + } + pub fn transfer_from(&mut self, args: TransferFromCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + guard.run(|| token.transfer_from(caller, args)) + }; + Self::emit_transfer(event); + } + pub fn mint(&mut self, args: MintCall) { + self.pausable.assert_not_paused(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + MINT_ACTION, + mint_payload_hash(args.to, args.token_id), + ), + ); + let event = self.token.mint(args.to, args.token_id); + Self::emit_transfer(event); + } + pub fn burn(&mut self, token_id: u64) { + self.pausable.assert_not_paused(); + let event = self.token.burn(caller(), token_id); + Self::emit_transfer(event); + } + pub fn pause(&mut self, args: AdminCall) { + self.pausable.assert_not_paused(); + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + PAUSE_ACTION, + empty_payload_hash(b"drc721.pause"), + ), + ); + self.pausable.pause(); + Self::emit_paused(caller); + } + pub fn unpause(&mut self, args: AdminCall) { + self.pausable.assert_paused(); + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + UNPAUSE_ACTION, + empty_payload_hash(b"drc721.unpause"), + ), + ); + self.pausable.unpause(); + Self::emit_unpaused(caller); + } + + pub fn paused(&self) -> bool { + self.pausable.paused() + } + pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { + validate_royalty(args.info); + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + SET_DEFAULT_ROYALTY_ACTION, + royalty_payload_hash(None, args.info), + ), + ); + self.royalties.set_default_royalty(args.info); + Self::emit_default_royalty_set(caller, args.info); + } + pub fn clear_default_royalty(&mut self, args: AdminCall) { + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + CLEAR_DEFAULT_ROYALTY_ACTION, + empty_payload_hash(b"drc721.clear_default_royalty"), + ), + ); + self.royalties.clear_default_royalty(); + Self::emit_default_royalty_cleared(caller); + } + pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { + validate_royalty(args.info); + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + SET_TOKEN_ROYALTY_ACTION, + royalty_payload_hash(Some(args.token_id), args.info), + ), + ); + self.royalties.set_token_royalty(args.token_id, args.info); + Self::emit_token_royalty_set(caller, args.token_id, args.info); + } + pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { + let caller = self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + NFT_ADMIN_DOMAIN, + CLEAR_TOKEN_ROYALTY_ACTION, + token_payload_hash(args.token_id), + ), + ); + self.royalties.clear_token_royalty(args.token_id); + Self::emit_token_royalty_cleared(caller, args.token_id); + } + + fn authorize_owner_action( + &mut self, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.ownable.authorize_owner_action( + &mut self.authorizations, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_transfer(event: Drc721Transfer) { + abi::emit( + TRANSFER_TOPIC, + Drc721Transfer { + from: event.from, + to: event.to, + token_id: event.token_id, + }, + ); + } + + fn emit_approval(event: Drc721Approval) { + abi::emit( + APPROVAL_TOPIC, + Drc721Approval { + owner: event.owner, + approved: event.approved, + token_id: event.token_id, + }, + ); + } + + fn emit_approval_for_all(event: Drc721ApprovalForAll) { + abi::emit( + APPROVAL_FOR_ALL_TOPIC, + Drc721ApprovalForAll { + owner: event.owner, + operator: event.operator, + approved: event.approved, + }, + ); + } + + fn emit_paused(account: Principal) { + abi::emit(PAUSED_TOPIC, Paused { account }); + } + + fn emit_unpaused(account: Principal) { + abi::emit(UNPAUSED_TOPIC, Unpaused { account }); + } + + fn emit_default_royalty_set(operator: Principal, info: RoyaltyInfo) { + abi::emit( + DEFAULT_ROYALTY_SET_TOPIC, + DefaultRoyaltySet { + operator, + receiver: info.receiver, + basis_points: info.basis_points, + }, + ); + } + + fn emit_default_royalty_cleared(operator: Principal) { + abi::emit( + DEFAULT_ROYALTY_CLEARED_TOPIC, + DefaultRoyaltyCleared { operator }, + ); + } + + fn emit_token_royalty_set( + operator: Principal, + token_id: u64, + info: RoyaltyInfo, + ) { + abi::emit( + TOKEN_ROYALTY_SET_TOPIC, + TokenRoyaltySet { + operator, + token_id, + receiver: info.receiver, + basis_points: info.basis_points, + }, + ); + } + + fn emit_token_royalty_cleared(operator: Principal, token_id: u64) { + abi::emit( + TOKEN_ROYALTY_CLEARED_TOPIC, + TokenRoyaltyCleared { operator, token_id }, + ); + } + } + + impl Default for Drc721Collection { + fn default() -> Self { + Self::new() + } + } + + fn caller() -> Principal { + CallContext::current().require_principal(error::UNAUTHORIZED) + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + abi::keccak256(bytes) + } + + fn royalty_payload_hash( + token_id: Option, + info: RoyaltyInfo, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.royalty"[..]); + match token_id { + Some(token_id) => { + bytes.push(1); + bytes.extend_from_slice(&token_id.to_be_bytes()); + } + None => bytes.push(0), + } + push_principal(&mut bytes, info.receiver); + bytes.extend_from_slice(&info.basis_points.to_be_bytes()); + abi::keccak256(bytes) + } + + fn token_payload_hash(token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.token"[..]); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn validate_royalty(info: RoyaltyInfo) { + if info.receiver.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if info.basis_points > MAX_BASIS_POINTS { + panic!("DRC721: royalty exceeds sale price"); + } + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} diff --git a/standards/examples/moonlight_call_router/Cargo.toml b/standards/examples/moonlight_call_router/Cargo.toml new file mode 100644 index 00000000..f4b6cffa --- /dev/null +++ b/standards/examples/moonlight_call_router/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "moonlight-call-router" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-forge = "=0.3.0" +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } + +[features] +contract = ["dusk-core/abi-dlmalloc"] +data-driver = [] diff --git a/standards/examples/moonlight_call_router/Makefile b/standards/examples/moonlight_call_router/Makefile new file mode 100644 index 00000000..816da2cd --- /dev/null +++ b/standards/examples/moonlight_call_router/Makefile @@ -0,0 +1,23 @@ +TARGET_DIR ?= ../../../target + +all: wasm clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p moonlight-call-router + +test: + +clippy: + @cargo clippy -p moonlight-call-router -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm test clippy doc diff --git a/standards/examples/moonlight_call_router/src/lib.rs b/standards/examples/moonlight_call_router/src/lib.rs new file mode 100644 index 00000000..358f49fe --- /dev/null +++ b/standards/examples/moonlight_call_router/src/lib.rs @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Test-only router that models the transfer-contract Moonlight call path. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as moonlight_call_router_types; + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +pub struct ForwardCall { + pub contract: ContractId, + pub function: String, + pub args: Vec, +} + +#[dusk_forge::contract] +mod moonlight_call_router { + use alloc::vec::Vec; + + use dusk_core::abi; + use moonlight_call_router_types::ForwardCall; + + pub struct MoonlightCallRouter; + + impl MoonlightCallRouter { + pub const fn new() -> Self { + Self + } + + pub fn init(&mut self) {} + + pub fn forward(&mut self, call: ForwardCall) -> Vec { + abi::call_raw(call.contract, &call.function, &call.args) + .unwrap_or_else(|err| panic!("MoonlightCallRouter: {err:?}")) + } + } + + impl Default for MoonlightCallRouter { + fn default() -> Self { + Self::new() + } + } +} diff --git a/standards/examples/multisig_controller/Cargo.toml b/standards/examples/multisig_controller/Cargo.toml new file mode 100644 index 00000000..3ee38a20 --- /dev/null +++ b/standards/examples/multisig_controller/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "multisig-controller" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/multisig_controller/Makefile b/standards/examples/multisig_controller/Makefile new file mode 100644 index 00000000..ba0153af --- /dev/null +++ b/standards/examples/multisig_controller/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p multisig-controller + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p multisig-controller + +clippy: + @cargo clippy -p multisig-controller -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/multisig_controller/src/lib.rs b/standards/examples/multisig_controller/src/lib.rs new file mode 100644 index 00000000..2007cb8b --- /dev/null +++ b/standards/examples/multisig_controller/src/lib.rs @@ -0,0 +1,506 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Forge reference multisig controller for Dusk standards contracts. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as multisig_controller_types; + +use alloc::vec::Vec; +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::governance::{ + MultisigApprovals, MultisigControllerConfig, MultisigOperationId, + MultisigTarget, Threshold, +}; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const MULTISIG_CONTROLLER_DOMAIN: NonceDomain = [41u8; 32]; +pub const PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +pub const CONFIRM_ACTION: [u8; 32] = [43u8; 32]; +pub const CANCEL_ACTION: [u8; 32] = [44u8; 32]; +pub const UPDATE_AUTHORITY_ACTION: [u8; 32] = [45u8; 32]; +pub const SET_TIME_LIMITS_ACTION: [u8; 32] = [46u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub config: MultisigControllerConfig, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Propose { + pub target: MultisigTarget, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Confirm { + pub id: MultisigOperationId, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Cancel { + pub id: MultisigOperationId, + pub approvals: MultisigApprovals, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpdateAuthority { + pub owners: Vec, + pub threshold: Threshold, + pub approvals: MultisigApprovals, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetTimeLimits { + pub proposal_ttl: u64, + pub tombstone_ttl: u64, + pub approvals: MultisigApprovals, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + pub principal: Principal, + pub domain: NonceDomain, +} + +#[dusk_forge::contract(events = [ + MultisigOperationProposed, + MultisigOperationConfirmed, + MultisigOperationExecuted, + MultisigOperationCancelled, + MultisigAuthorityUpdated, + MultisigTimeLimitsUpdated, +])] +mod multisig_controller { + use alloc::format; + use alloc::vec::Vec; + + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, Principal, PrincipalKind, + }; + use dusk_contract_standards::governance::{ + MultisigAuthorityUpdated, MultisigController, + MultisigControllerOutcome, MultisigControllerStatus, + MultisigOperationCancelled, MultisigOperationConfirmed, + MultisigOperationExecuted, MultisigOperationId, + MultisigOperationProposed, MultisigPendingOperation, MultisigTarget, + MultisigTimeLimitsUpdated, Threshold, MULTISIG_AUTHORITY_UPDATED_TOPIC, + MULTISIG_OPERATION_CANCELLED_TOPIC, MULTISIG_OPERATION_CONFIRMED_TOPIC, + MULTISIG_OPERATION_EXECUTED_TOPIC, MULTISIG_OPERATION_PROPOSED_TOPIC, + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + }; + use dusk_core::abi; + + use multisig_controller_types::{ + Cancel, Confirm, Init, NonceQuery, Propose, SetTimeLimits, + UpdateAuthority, CANCEL_ACTION, CONFIRM_ACTION, + MULTISIG_CONTROLLER_DOMAIN, PROPOSE_ACTION, SET_TIME_LIMITS_ACTION, + UPDATE_AUTHORITY_ACTION, + }; + + pub struct MultisigControllerContract { + controller: MultisigController, + authorizations: AuthorizationManager, + initialized: bool, + } + + impl MultisigControllerContract { + pub const fn new() -> Self { + Self { + controller: MultisigController::new(), + authorizations: AuthorizationManager::new(), + initialized: false, + } + } + pub fn init(&mut self, args: Init) { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.controller.init(args.config); + self.initialized = true; + } + + pub fn owners(&self) -> Vec { + self.controller.owners() + } + + pub fn threshold(&self) -> Threshold { + self.controller.threshold() + } + + pub fn proposal_ttl(&self) -> u64 { + self.controller.proposal_ttl() + } + + pub fn tombstone_ttl(&self) -> u64 { + self.controller.tombstone_ttl() + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn operation_id( + &self, + target: MultisigTarget, + ) -> MultisigOperationId { + operation_id(&target) + } + + pub fn proposal( + &self, + id: MultisigOperationId, + ) -> Option { + self.controller.proposal(id) + } + + pub fn tombstone_expiry(&self, id: MultisigOperationId) -> Option { + self.controller.tombstone_expiry(id) + } + + pub fn propose(&mut self, args: Propose) -> Vec { + self.assert_initialized(); + let Propose { + target, + authorization, + } = args; + let id = operation_id(&target); + let authorizer = + self.verify_owner(PROPOSE_ACTION, id, authorization.as_ref()); + let outcome = + self.controller.propose(id, target, authorizer, now()); + self.consume_authorization(authorization.as_ref()); + match outcome.status { + MultisigControllerStatus::Proposed => abi::emit( + MULTISIG_OPERATION_PROPOSED_TOPIC, + MultisigOperationProposed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + MultisigControllerStatus::Confirmed + | MultisigControllerStatus::Ready => abi::emit( + MULTISIG_OPERATION_CONFIRMED_TOPIC, + MultisigOperationConfirmed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + } + let execution_id = outcome.id; + let (return_data, execution) = self.execute_if_ready(outcome); + if let Some((success, error)) = execution { + abi::emit( + MULTISIG_OPERATION_EXECUTED_TOPIC, + MultisigOperationExecuted { + id: execution_id, + success, + return_data: return_data.clone(), + error, + }, + ); + } + return_data + } + + pub fn confirm(&mut self, args: Confirm) -> Vec { + self.assert_initialized(); + let authorizer = self.verify_owner( + CONFIRM_ACTION, + args.id, + args.authorization.as_ref(), + ); + let outcome = self.controller.confirm(args.id, authorizer, now()); + self.consume_authorization(args.authorization.as_ref()); + match outcome.status { + MultisigControllerStatus::Proposed => abi::emit( + MULTISIG_OPERATION_PROPOSED_TOPIC, + MultisigOperationProposed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + MultisigControllerStatus::Confirmed + | MultisigControllerStatus::Ready => abi::emit( + MULTISIG_OPERATION_CONFIRMED_TOPIC, + MultisigOperationConfirmed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + } + let execution_id = outcome.id; + let (return_data, execution) = self.execute_if_ready(outcome); + if let Some((success, error)) = execution { + abi::emit( + MULTISIG_OPERATION_EXECUTED_TOPIC, + MultisigOperationExecuted { + id: execution_id, + success, + return_data: return_data.clone(), + error, + }, + ); + } + return_data + } + + pub fn cancel(&mut self, args: Cancel) { + self.assert_initialized(); + let signers = self.verify_quorum( + CANCEL_ACTION, + args.id, + &args.approvals.approvals, + ); + let event: MultisigOperationCancelled = + self.controller.cancel(args.id, &signers, now()); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_OPERATION_CANCELLED_TOPIC, + MultisigOperationCancelled { + id: event.id, + signers: event.signers, + }, + ); + } + + pub fn update_authority(&mut self, args: UpdateAuthority) { + self.assert_initialized(); + let payload_hash = + authority_payload_hash(&args.owners, args.threshold); + let signers = self.verify_quorum( + UPDATE_AUTHORITY_ACTION, + payload_hash, + &args.approvals.approvals, + ); + let event: MultisigAuthorityUpdated = self + .controller + .update_authority(&signers, args.owners, args.threshold); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_AUTHORITY_UPDATED_TOPIC, + MultisigAuthorityUpdated { + previous_owners: event.previous_owners, + previous_threshold: event.previous_threshold, + owners: event.owners, + threshold: event.threshold, + removed_operations: event.removed_operations, + }, + ); + } + + pub fn set_time_limits(&mut self, args: SetTimeLimits) { + self.assert_initialized(); + let payload_hash = + time_limits_payload_hash(args.proposal_ttl, args.tombstone_ttl); + let signers = self.verify_quorum( + SET_TIME_LIMITS_ACTION, + payload_hash, + &args.approvals.approvals, + ); + let event: MultisigTimeLimitsUpdated = + self.controller.set_time_limits( + &signers, + args.proposal_ttl, + args.tombstone_ttl, + ); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + MultisigTimeLimitsUpdated { + previous_proposal_ttl: event.previous_proposal_ttl, + previous_tombstone_ttl: event.previous_tombstone_ttl, + proposal_ttl: event.proposal_ttl, + tombstone_ttl: event.tombstone_ttl, + }, + ); + } + + fn verify_owner( + &self, + action_id: [u8; 32], + payload_hash: [u8; 32], + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = CallContext::current().principal { + match principal.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if self.controller.is_owner(principal) { + return principal; + } + } + PrincipalKind::Phoenix => {} + } + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let envelope = ActionEnvelope::for_current_chain( + abi::self_id(), + MULTISIG_CONTROLLER_DOMAIN, + action_id, + payload_hash, + ); + let principal = self.authorizations.verify_signed_action( + authorization, + envelope, + now(), + ); + if !self.controller.is_owner(principal) { + panic!("{}", error::UNAUTHORIZED); + } + principal + } + + fn verify_quorum( + &self, + action_id: [u8; 32], + payload_hash: [u8; 32], + approvals: &[SignedAuthorization], + ) -> Vec { + self.controller.verify_action( + &self.authorizations, + CallContext::current(), + approvals, + ActionEnvelope::for_current_chain( + abi::self_id(), + MULTISIG_CONTROLLER_DOMAIN, + action_id, + payload_hash, + ), + now(), + ) + } + + fn consume_authorization( + &mut self, + authorization: Option<&SignedAuthorization>, + ) { + if let Some(authorization) = authorization { + self.authorizations.consume_verified(authorization); + } + } + + fn consume_approvals(&mut self, approvals: &[SignedAuthorization]) { + for approval in approvals { + self.authorizations.consume_verified(approval); + } + } + + fn execute_if_ready( + &mut self, + outcome: MultisigControllerOutcome, + ) -> (Vec, Option<(bool, Option)>) { + let Some(operation) = outcome.ready_operation else { + return (Vec::new(), None); + }; + + let call = &operation.target.call; + let (success, return_data, error) = match abi::call_raw( + call.contract, + &call.fn_name, + &call.fn_args, + ) { + Ok(data) => (true, data, None), + Err(error) => (false, Vec::new(), Some(format!("{error}"))), + }; + (return_data, Some((success, error))) + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + } + + impl Default for MultisigControllerContract { + fn default() -> Self { + Self::new() + } + } + + fn operation_id(target: &MultisigTarget) -> MultisigOperationId { + let mut bytes = + Vec::from(&b"dusk-contract-standards/multisig/op/v1"[..]); + bytes.push(abi::chain_id()); + bytes.extend_from_slice(&abi::self_id().to_bytes()); + bytes.extend_from_slice(&target.call.to_var_bytes()); + bytes.extend_from_slice(&target.salt); + abi::keccak256(bytes) + } + + fn authority_payload_hash( + owners: &[Principal], + threshold: Threshold, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"multisig.authority"[..]); + bytes.extend_from_slice(&(owners.len() as u32).to_be_bytes()); + for owner in owners { + push_principal(&mut bytes, *owner); + } + bytes.extend_from_slice(&threshold.to_be_bytes()); + abi::keccak256(bytes) + } + + fn time_limits_payload_hash( + proposal_ttl: u64, + tombstone_ttl: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"multisig.time_limits"[..]); + bytes.extend_from_slice(&proposal_ttl.to_be_bytes()); + bytes.extend_from_slice(&tombstone_ttl.to_be_bytes()); + abi::keccak256(bytes) + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} diff --git a/standards/examples/proxy_counter/Cargo.toml b/standards/examples/proxy_counter/Cargo.toml new file mode 100644 index 00000000..806a8c94 --- /dev/null +++ b/standards/examples/proxy_counter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "proxy-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/proxy_counter/Makefile b/standards/examples/proxy_counter/Makefile new file mode 100644 index 00000000..68427ebe --- /dev/null +++ b/standards/examples/proxy_counter/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p proxy-counter + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p proxy-counter + +clippy: + @cargo clippy -p proxy-counter -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs new file mode 100644 index 00000000..a4553ec6 --- /dev/null +++ b/standards/examples/proxy_counter/src/lib.rs @@ -0,0 +1,390 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Dusk-native proxy/state-store example. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as proxy_counter_types; + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; +pub const SET_VALUE_ACTION: [u8; 32] = [32u8; 32]; +pub const PREPARE_UPGRADE_ACTION: [u8; 32] = [33u8; 32]; +pub const ACTIVATE_UPGRADE_ACTION: [u8; 32] = [34u8; 32]; +pub const CANCEL_UPGRADE_ACTION: [u8; 32] = [35u8; 32]; +pub const ROLLBACK_ACTION: [u8; 32] = [36u8; 32]; +pub const FINALIZE_ROLLBACK_ACTION: [u8; 32] = [37u8; 32]; +pub const VALUE_CHANGED_TOPIC: &str = "proxy_counter/value_changed"; + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub admin: Principal, + pub implementation: ContractId, + pub upgrade_delay: u64, + pub rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PrepareUpgrade { + pub implementation: ContractId, + pub migrate_data: Vec, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValue { + pub value: u64, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ValueChanged { + pub previous: u64, + pub value: u64, + pub sender: Option, +} + +impl dusk_forge::ContractEvent for ValueChanged { + const TOPICS: &'static [&'static str] = &[VALUE_CHANGED_TOPIC]; +} + +#[dusk_forge::contract(events = [ + ValueChanged, + UpgradePrepared, + UpgradeActivated, + UpgradeCancelled, + UpgradeRolledBack, + RollbackFinalized, +])] +mod proxy_counter { + use alloc::vec::Vec; + + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::proxy::{ + RollbackFinalized, StateStore, UpgradeActivated, UpgradeAdmin, + UpgradeCancelled, UpgradePrepared, UpgradeRolledBack, + ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, + UPGRADE_CANCELLED_TOPIC, UPGRADE_PREPARED_TOPIC, + UPGRADE_ROLLED_BACK_TOPIC, + }; + use dusk_core::abi::{self, ContractId}; + use proxy_counter_types::{ + AdminCall, Init, PrepareUpgrade, SetValue, ValueChanged, + ACTIVATE_UPGRADE_ACTION, CANCEL_UPGRADE_ACTION, + FINALIZE_ROLLBACK_ACTION, PREPARE_UPGRADE_ACTION, PROXY_ADMIN_DOMAIN, + ROLLBACK_ACTION, SET_VALUE_ACTION, VALUE_CHANGED_TOPIC, + }; + + const COUNTER_KEY: [u8; 32] = [7u8; 32]; + + pub struct ProxyCounter { + store: StateStore, + admin: Option, + authorizations: AuthorizationManager, + } + + impl ProxyCounter { + pub const fn new() -> Self { + Self { + store: StateStore::new(), + admin: None, + authorizations: AuthorizationManager::new(), + } + } + pub fn init(&mut self, args: Init) { + if self.admin.is_some() { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.admin = Some(UpgradeAdmin::new( + args.admin, + args.implementation, + args.upgrade_delay, + args.rollback_window, + )); + } + + pub fn implementation(&self) -> ContractId { + self.admin().implementation() + } + + pub fn rollback_deadline(&self) -> u64 { + self.admin().rollback_deadline() + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn value(&self) -> u64 { + u64::from_be_bytes( + self.store.get_word(COUNTER_KEY)[24..32].try_into().unwrap(), + ) + } + pub fn increment(&mut self) { + let previous = self.value(); + let next = previous.checked_add(1).expect(error::OVERFLOW); + self.write_value(next); + Self::emit_value_changed( + previous, + next, + CallContext::current().principal, + ); + } + pub fn set_value(&mut self, args: SetValue) { + let previous = self.value(); + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + SET_VALUE_ACTION, + value_payload_hash(args.value), + ), + ); + self.write_value(args.value); + Self::emit_value_changed(previous, args.value, Some(caller)); + } + pub fn prepare_upgrade(&mut self, args: PrepareUpgrade) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + PREPARE_UPGRADE_ACTION, + prepare_payload_hash( + args.implementation, + &args.migrate_data, + ), + ), + ); + let event = self.admin_mut().prepare( + caller, + args.implementation, + now(), + args.migrate_data, + ); + Self::emit_upgrade_prepared(event); + } + pub fn activate_upgrade(&mut self, args: AdminCall) -> Vec { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + ACTIVATE_UPGRADE_ACTION, + empty_payload_hash(b"proxy.activate_upgrade"), + ), + ); + let (migrate_data, event) = + self.admin_mut().activate_with_event(caller, now()); + Self::emit_upgrade_activated(event); + migrate_data + } + pub fn cancel_pending_upgrade(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + CANCEL_UPGRADE_ACTION, + empty_payload_hash(b"proxy.cancel_upgrade"), + ), + ); + let event = self.admin_mut().cancel_pending(caller); + Self::emit_upgrade_cancelled(event); + } + pub fn rollback(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + ROLLBACK_ACTION, + empty_payload_hash(b"proxy.rollback"), + ), + ); + let event = self.admin_mut().rollback_with_event(caller, now()); + Self::emit_upgrade_rolled_back(event); + } + pub fn finalize_rollback_window(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::for_current_chain( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + FINALIZE_ROLLBACK_ACTION, + empty_payload_hash(b"proxy.finalize_rollback"), + ), + ); + let event = self + .admin_mut() + .finalize_rollback_window_with_event(caller, now()); + Self::emit_rollback_finalized(event); + } + + fn write_value(&mut self, value: u64) { + let mut word = [0u8; 32]; + word[24..32].copy_from_slice(&value.to_be_bytes()); + self.store.set_word(COUNTER_KEY, word); + } + + fn authorize_admin_action( + &mut self, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + let admin = self.admin().admin(); + self.authorizations.authorize_principal_action( + admin, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_upgrade_prepared(event: UpgradePrepared) { + abi::emit( + UPGRADE_PREPARED_TOPIC, + UpgradePrepared { + implementation: event.implementation, + eta: event.eta, + }, + ); + } + + fn emit_upgrade_activated(event: UpgradeActivated) { + abi::emit( + UPGRADE_ACTIVATED_TOPIC, + UpgradeActivated { + previous_implementation: event.previous_implementation, + implementation: event.implementation, + rollback_deadline: event.rollback_deadline, + }, + ); + } + + fn emit_upgrade_cancelled(event: UpgradeCancelled) { + abi::emit( + UPGRADE_CANCELLED_TOPIC, + UpgradeCancelled { + implementation: event.implementation, + }, + ); + } + + fn emit_upgrade_rolled_back(event: UpgradeRolledBack) { + abi::emit( + UPGRADE_ROLLED_BACK_TOPIC, + UpgradeRolledBack { + from_implementation: event.from_implementation, + restored_implementation: event.restored_implementation, + }, + ); + } + + fn emit_rollback_finalized(event: RollbackFinalized) { + abi::emit( + ROLLBACK_FINALIZED_TOPIC, + RollbackFinalized { + implementation: event.implementation, + }, + ); + } + + fn emit_value_changed( + previous: u64, + value: u64, + sender: Option, + ) { + abi::emit( + VALUE_CHANGED_TOPIC, + ValueChanged { + previous, + value, + sender, + }, + ); + } + + fn admin(&self) -> &UpgradeAdmin { + self.admin + .as_ref() + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)) + } + + fn admin_mut(&mut self) -> &mut UpgradeAdmin { + self.admin + .as_mut() + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)) + } + } + + impl Default for ProxyCounter { + fn default() -> Self { + Self::new() + } + } + + fn now() -> u64 { + abi::block_height() + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + abi::keccak256(bytes) + } + + fn prepare_payload_hash( + implementation: ContractId, + migrate_data: &[u8], + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.prepare"[..]); + bytes.extend_from_slice(&implementation.to_bytes()); + bytes.extend_from_slice(&(migrate_data.len() as u32).to_be_bytes()); + bytes.extend_from_slice(migrate_data); + abi::keccak256(bytes) + } +}