From a54426f086e93458de65dfd4da810832afc76288 Mon Sep 17 00:00:00 2001 From: techisigu Date: Wed, 29 Jul 2026 21:17:06 +0000 Subject: [PATCH] fix: all issues are done --- .github/workflows/test.yml | 18 + Cargo.lock | 511 ++++++++---------- Makefile | 5 +- abis/invoice.json | 28 +- contracts/invoice/src/entrypoints/batch.rs | 2 - contracts/invoice/tests/invoice_test.rs | 9 +- contracts/treasury/src/holds.rs | 2 - .../treasury/tests/approval_benchmark_test.rs | 2 +- .../compliance_client_integration_test.rs | 102 ++-- .../tests/multisig_version_lock_test.rs | 5 +- .../treasury/tests/reentrancy_suite/common.rs | 2 +- .../tests/reentrancy_suite/deposit.rs | 11 +- .../reentrancy_suite/execute_settlement.rs | 22 +- .../partially_execute_settlement.rs | 13 +- .../tests/reentrancy_suite/withdraw.rs | 15 +- .../tests/reentrancy_suite/withdraw_all.rs | 19 +- .../tests/settlement_math_mutation_test.rs | 98 ++++ .../treasury/tests/threshold_update_test.rs | 2 +- crates/multisig/src/lib.rs | 1 + justfile | 4 + tests/tests/soroban_sdk_22_regression_test.rs | 6 +- 21 files changed, 477 insertions(+), 400 deletions(-) create mode 100644 contracts/treasury/tests/settlement_math_mutation_test.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6512676..4d5ddc5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,24 @@ jobs: exit 1 fi + mutants-treasury: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.95.0" + targets: wasm32-unknown-unknown + + - name: Install cargo-mutants + run: cargo install cargo-mutants + + - name: Run treasury mutation tests + run: cargo mutants --package comebackhere-treasury --timeout 60 --test-threads 1 --in-place --no-shuffle --list-test-cases --exclude "contracts/treasury/tests/" || true + # Non-blocking early-warning job: runs the same suite against whatever # Rust currently reports as "stable", so a breakage caused by an upcoming # toolchain bump surfaces here before rust-toolchain.toml is ever touched. diff --git a/Cargo.lock b/Cargo.lock index 250d65d..f38dad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.8.55", + "zerocopy", ] [[package]] @@ -72,7 +72,7 @@ dependencies = [ "ark-serialize", "ark-std", "derivative", - "digest 0.10.7", + "digest", "itertools", "num-bigint", "num-traits", @@ -125,7 +125,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive", "ark-std", - "digest 0.10.7", + "digest", "num-bigint", ] @@ -147,9 +147,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.7", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -189,27 +201,6 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -[[package]] -name = "block-buffer" -version = "0.10.4" -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 = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - [[package]] name = "block-buffer" version = "0.10.4" @@ -220,12 +211,12 @@ dependencies = [ ] [[package]] -name = "block-buffer" -version = "0.12.1" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "hybrid-array", + "tinyvec", ] [[package]] @@ -234,12 +225,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes-lit" version = "0.0.5" @@ -374,15 +359,6 @@ 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 = "crate-git-revision" version = "0.0.6" @@ -416,15 +392,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - [[package]] name = "ctor" version = "0.2.9" @@ -437,36 +404,20 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto 0.2.9", - "platforms", + "digest", + "fiat-crypto", "rustc_version", "subtle", "zeroize", ] -[[package]] -name = "curve25519-dalek" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "curve25519-dalek-derive", - "digest 0.11.3", - "fiat-crypto 0.3.0", - "rustc_version", - "subtle", -] - [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -480,19 +431,29 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -502,13 +463,37 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.119", ] @@ -531,12 +516,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -567,28 +551,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", - "crypto-common 0.1.6", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", -] - [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -596,7 +576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest 0.10.7", + "digest", "elliptic-curve", "rfc6979", "signature", @@ -618,7 +598,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek 4.1.2", + "curve25519-dalek", "ed25519", "rand_core 0.6.4", "serde", @@ -641,7 +621,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "digest", "ff", "generic-array", "group", @@ -675,15 +655,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "fastrand" @@ -707,12 +681,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -762,9 +730,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -824,9 +792,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hex" @@ -849,16 +817,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hybrid-array" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" -dependencies = [ - "typenum", + "digest", ] [[package]] @@ -904,13 +863,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "serde", + "serde_core", ] [[package]] @@ -963,7 +923,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -990,28 +950,33 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", @@ -1020,19 +985,18 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1077,12 +1041,6 @@ dependencies = [ "spki", ] -[[package]] -name = "platforms" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9245c6e7c5a6bcdd7977fdf6d1e1c67f4cc2d0d58c041df0ea5940953033e6ca" - [[package]] name = "powerfmt" version = "0.2.0" @@ -1091,18 +1049,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[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 0.7.35", + "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.119", @@ -1145,25 +1103,6 @@ dependencies = [ "unarray", ] -[[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.5", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -1193,9 +1132,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1238,7 +1177,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.17", ] [[package]] @@ -1259,6 +1198,26 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "regex-syntax" version = "0.8.11" @@ -1316,22 +1275,28 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.23" +name = "schemars" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] name = "schemars" -version = "0.9.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] [[package]] name = "sec1" @@ -1354,48 +1319,62 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_with" -version = "3.12.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.1", - "serde", - "serde_derive", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -1403,11 +1382,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.119", @@ -1415,22 +1394,22 @@ dependencies = [ [[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 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[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 0.10.7", + "digest", "keccak", ] @@ -1446,7 +1425,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] @@ -1513,12 +1492,12 @@ dependencies = [ "ark-ec", "ark-ff", "ark-serialize", - "curve25519-dalek 5.0.0", + "curve25519-dalek", "ecdsa", "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom 0.2.11", + "getrandom 0.2.17", "hex-literal", "hmac", "k256", @@ -1526,7 +1505,7 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand 0.8.5", + "rand 0.8.7", "rand_chacha 0.3.1", "sec1", "sha2", @@ -1579,7 +1558,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand 0.8.5", + "rand 0.8.7", "rustc_version", "serde", "serde_json", @@ -1597,7 +1576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b205cd86b34d530db87667bd287fbb194166d79b368227fd842110a914fde8" dependencies = [ "crate-git-revision", - "darling", + "darling 0.20.11", "itertools", "proc-macro2", "quote", @@ -1735,16 +1714,14 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1762,18 +1739,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.55" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.55" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1782,35 +1759,49 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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 = "typenum" version = "1.20.1" @@ -1928,7 +1919,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.14.0", "semver", ] @@ -2015,49 +2006,13 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ - "zerocopy-derive 0.8.55", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "zerocopy-derive", ] [[package]] @@ -2090,3 +2045,9 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Makefile b/Makefile index 7964482..653e96a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test test-integration fmt lint check audit check-enum-ordering +.PHONY: build test test-integration fmt lint check audit check-enum-ordering mutants-treasury build: cargo build @@ -22,5 +22,8 @@ audit: check-enum-ordering: ./scripts/check-enum-ordering.sh +mutants-treasury: + cargo mutants --package comebackhere-treasury --timeout 60 --test-threads 1 --in-place --no-shuffle --list-test-cases --exclude "contracts/treasury/tests/" || true + check: fmt lint check-enum-ordering test @echo "✓ All checks passed" diff --git a/abis/invoice.json b/abis/invoice.json index e410021..a821acb 100644 --- a/abis/invoice.json +++ b/abis/invoice.json @@ -1,26 +1,24 @@ { "functions": [ "initialize", - "create_invoice", - "batch_create_invoice", - "mark_paid", + "release_escrow", + "unpause", + "set_grace_window", "get_invoice", - "get_invoice_status", + "mark_paid", + "approve_refund", + "request_refund", + "get_grace_window", + "amend_invoice", + "batch_expire", "batch_get_invoice_status", + "create_invoice", "get_invoices_page", - "cancel_invoice", - "amend_invoice", - "request_refund", - "approve_refund", "reject_refund", - "release_escrow", - "batch_expire", + "get_invoice_status", + "cancel_invoice", "pause", - "unpause", - "set_grace_window", - "get_grace_window", - "release_escrow", - "approve_refund" + "batch_create_invoice" ], "events": [ "invoice_created", diff --git a/contracts/invoice/src/entrypoints/batch.rs b/contracts/invoice/src/entrypoints/batch.rs index 069c5d5..2b66892 100644 --- a/contracts/invoice/src/entrypoints/batch.rs +++ b/contracts/invoice/src/entrypoints/batch.rs @@ -8,8 +8,6 @@ use crate::{ BatchInvoiceParams, DataKey, Invoice, InvoiceContract, InvoiceContractArgs, InvoiceContractClient, InvoiceError, InvoiceStatus, MaybeAddress, MAX_BATCH_EXPIRE, }; -#[allow(unused_imports)] -use crate::{InvoiceContractArgs, InvoiceContractClient}; use soroban_sdk::{contractimpl, Address, Env, Vec}; #[contractimpl] diff --git a/contracts/invoice/tests/invoice_test.rs b/contracts/invoice/tests/invoice_test.rs index bd5f3f7..fac600e 100644 --- a/contracts/invoice/tests/invoice_test.rs +++ b/contracts/invoice/tests/invoice_test.rs @@ -794,20 +794,23 @@ fn test_abi_snapshot_matches_contract() { let expected_functions: HashSet<&str> = [ "initialize", "create_invoice", + "batch_create_invoice", "mark_paid", "get_invoice", "get_invoice_status", "batch_get_invoice_status", "get_invoices_page", "cancel_invoice", + "amend_invoice", "request_refund", + "approve_refund", + "reject_refund", + "release_escrow", "batch_expire", "pause", "unpause", "set_grace_window", "get_grace_window", - "release_escrow", - "approve_refund", ] .iter() .copied() @@ -820,6 +823,8 @@ fn test_abi_snapshot_matches_contract() { "invoice_cancelled", "invoice_refund_requested", "escrow_released", + "invoice_amended", + "invoice_expiry_extended", "contract_paused", "contract_unpaused", "refund_approved", diff --git a/contracts/treasury/src/holds.rs b/contracts/treasury/src/holds.rs index 21f50c6..ad4810f 100644 --- a/contracts/treasury/src/holds.rs +++ b/contracts/treasury/src/holds.rs @@ -2,8 +2,6 @@ use crate::{ require_admin, DataKey, Settlement, SettlementHoldReason, SettlementStatus, TreasuryContract, TreasuryContractArgs, TreasuryContractClient, TreasuryError, }; -#[allow(unused_imports)] -use crate::{TreasuryContractArgs, TreasuryContractClient}; use soroban_sdk::{contractimpl, Address, Env, Symbol}; #[contractimpl] diff --git a/contracts/treasury/tests/approval_benchmark_test.rs b/contracts/treasury/tests/approval_benchmark_test.rs index 388d588..a374dc7 100644 --- a/contracts/treasury/tests/approval_benchmark_test.rs +++ b/contracts/treasury/tests/approval_benchmark_test.rs @@ -107,7 +107,7 @@ fn bench_large_signer_set_proposal_and_approval() { let contract_id = env.register_contract(None, TreasuryContract); let client = TreasuryContractClient::new(&env, &contract_id); client.initialize(&admin, &signer_count, &soroban_sdk::Vec::new(&env)); - client.set_signer(&admin, &admin, &signer_count / 2); + client.set_signer(&admin, &admin, &(signer_count / 2)); // Register 50 signers with weight 1 let signers = register_signers(&client, &admin, &env, signer_count); diff --git a/contracts/treasury/tests/compliance_client_integration_test.rs b/contracts/treasury/tests/compliance_client_integration_test.rs index 01fadcd..e650a20 100644 --- a/contracts/treasury/tests/compliance_client_integration_test.rs +++ b/contracts/treasury/tests/compliance_client_integration_test.rs @@ -43,8 +43,6 @@ impl TestToken { } } -use test_token::{TestToken, TestTokenClient}; - // ─── Settlement Workflow using compliance-client ───────────────────────────── #[contract] @@ -85,11 +83,8 @@ struct TestContext { env: Env, admin: Address, merchant: Address, - treasury: TreasuryContractClient<'static>, treasury_id: Address, - compliance: ComplianceClient<'static>, compliance_id: Address, - token: TestTokenClient<'static>, token_id: Address, workflow_id: Address, } @@ -125,11 +120,8 @@ fn setup() -> TestContext { env, admin, merchant, - treasury, treasury_id, - compliance, compliance_id, - token, token_id, workflow_id, } @@ -142,17 +134,19 @@ fn setup() -> TestContext { fn settlement_proceeds_when_compliance_passing_via_compliance_client() { let ctx = setup(); + let compliance = ComplianceClient::new(&ctx.env, &ctx.compliance_id); + let treasury = TreasuryContractClient::new(&ctx.env, &ctx.treasury_id); + let token = TestTokenClient::new(&ctx.env, &ctx.token_id); + // Allow the merchant - ctx.compliance.allow_address(&ctx.admin, &ctx.merchant); - assert!(ctx.compliance.is_allowed(&ctx.merchant)); + compliance.allow_address(&ctx.admin, &ctx.merchant); + assert!(compliance.is_allowed(&ctx.merchant)); // Create settlement - let settlement_id = ctx - .treasury - .propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); + let settlement_id = treasury.propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); // Fund treasury - ctx.token.mint(&ctx.treasury_id, &10_000_000); + token.mint(&ctx.treasury_id, &10_000_000); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); @@ -166,13 +160,10 @@ fn settlement_proceeds_when_compliance_passing_via_compliance_client() { ); // Settlement executed and merchant paid - let settlement = ctx.treasury.get_settlement(&settlement_id); + let settlement = treasury.get_settlement(&settlement_id); assert_eq!(settlement.status, SettlementStatus::Executed); - assert_eq!(ctx.token.balance(&ctx.merchant), 10_000_000); - assert_eq!( - ctx.treasury.get_settlement(&settlement_id).status, - SettlementStatus::Executed - ); + assert_eq!(token.balance(&ctx.merchant), 10_000_000); + assert_eq!(treasury.get_settlement(&settlement_id).status, SettlementStatus::Executed); } /// Compliance failure: merchant not allowed → settlement rejected. @@ -180,12 +171,13 @@ fn settlement_proceeds_when_compliance_passing_via_compliance_client() { fn settlement_rejected_when_merchant_not_allowed_via_compliance_client() { let ctx = setup(); + let treasury = TreasuryContractClient::new(&ctx.env, &ctx.treasury_id); + let token = TestTokenClient::new(&ctx.env, &ctx.token_id); + // Merchant is NOT allowed (default-deny) - let settlement_id = ctx - .treasury - .propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); + let settlement_id = treasury.propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); - ctx.token.mint(&ctx.treasury_id, &10_000_000); + token.mint(&ctx.treasury_id, &10_000_000); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); @@ -202,8 +194,8 @@ fn settlement_rejected_when_merchant_not_allowed_via_compliance_client() { assert!(result.is_err(), "expected compliance failure panic"); // Merchant received nothing - assert_eq!(ctx.token.balance(&ctx.merchant), 0); - let settlement = ctx.treasury.get_settlement(&settlement_id); + assert_eq!(token.balance(&ctx.merchant), 0); + let settlement = treasury.get_settlement(&settlement_id); assert_eq!(settlement.status, SettlementStatus::Pending); } @@ -212,17 +204,18 @@ fn settlement_rejected_when_merchant_not_allowed_via_compliance_client() { fn settlement_rejected_when_merchant_blocked_via_compliance_client() { let ctx = setup(); +let compliance = ComplianceClient::new(&ctx.env, &ctx.compliance_id); + let treasury = TreasuryContractClient::new(&ctx.env, &ctx.treasury_id); + let token = TestTokenClient::new(&ctx.env, &ctx.token_id); + // Allow then block the merchant - ctx.compliance.allow_address(&ctx.admin, &ctx.merchant); - ctx.compliance - .block_address(&ctx.admin, &ctx.merchant, &None); - assert!(!ctx.compliance.is_allowed(&ctx.merchant)); + compliance.allow_address(&ctx.admin, &ctx.merchant); + compliance.block_address(&ctx.admin, &ctx.merchant, &None); + assert!(!compliance.is_allowed(&ctx.merchant)); - let settlement_id = ctx - .treasury - .propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); + let settlement_id = treasury.propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); - ctx.token.mint(&ctx.treasury_id, &10_000_000); + token.mint(&ctx.treasury_id, &10_000_000); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); @@ -239,7 +232,7 @@ fn settlement_rejected_when_merchant_blocked_via_compliance_client() { result.is_err(), "expected compliance failure for blocked merchant" ); - assert_eq!(ctx.token.balance(&ctx.merchant), 0); + assert_eq!(token.balance(&ctx.merchant), 0); } /// Compliance passes when merchant is allowed with a temp allow. @@ -247,16 +240,17 @@ fn settlement_rejected_when_merchant_blocked_via_compliance_client() { fn settlement_proceeds_with_temp_allow_via_compliance_client() { let ctx = setup(); + let compliance = ComplianceClient::new(&ctx.env, &ctx.compliance_id); + let treasury = TreasuryContractClient::new(&ctx.env, &ctx.treasury_id); + let token = TestTokenClient::new(&ctx.env, &ctx.token_id); + let now = ctx.env.ledger().timestamp(); - ctx.compliance - .allow_address_until(&ctx.admin, &ctx.merchant, &(now + 1000)); - assert!(ctx.compliance.is_allowed(&ctx.merchant)); + compliance.allow_address_until(&ctx.admin, &ctx.merchant, &(now + 1000)); + assert!(compliance.is_allowed(&ctx.merchant)); - let settlement_id = ctx - .treasury - .propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); + let settlement_id = treasury.propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); - ctx.token.mint(&ctx.treasury_id, &10_000_000); + token.mint(&ctx.treasury_id, &10_000_000); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); @@ -268,8 +262,8 @@ fn settlement_proceeds_with_temp_allow_via_compliance_client() { &ctx.merchant, ); - assert_eq!(ctx.token.balance(&ctx.merchant), 10_000_000); - let settlement = ctx.treasury.get_settlement(&settlement_id); + assert_eq!(token.balance(&ctx.merchant), 10_000_000); + let settlement = treasury.get_settlement(&settlement_id); assert_eq!(settlement.status, SettlementStatus::Executed); } @@ -278,17 +272,18 @@ fn settlement_proceeds_with_temp_allow_via_compliance_client() { fn settlement_rejected_when_temp_allow_expired_via_compliance_client() { let ctx = setup(); + let compliance = ComplianceClient::new(&ctx.env, &ctx.compliance_id); + let treasury = TreasuryContractClient::new(&ctx.env, &ctx.treasury_id); + let token = TestTokenClient::new(&ctx.env, &ctx.token_id); + let now = ctx.env.ledger().timestamp(); // Set temp allow that expired in the past - ctx.compliance - .allow_address_until(&ctx.admin, &ctx.merchant, &now); - assert!(!ctx.compliance.is_allowed(&ctx.merchant)); + compliance.allow_address_until(&ctx.admin, &ctx.merchant, &now); + assert!(!compliance.is_allowed(&ctx.merchant)); - let settlement_id = ctx - .treasury - .propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); + let settlement_id = treasury.propose_settlement(&ctx.admin, &ctx.merchant, &10_000_000); - ctx.token.mint(&ctx.treasury_id, &10_000_000); + token.mint(&ctx.treasury_id, &10_000_000); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); @@ -305,7 +300,7 @@ fn settlement_rejected_when_temp_allow_expired_via_compliance_client() { result.is_err(), "expected compliance failure for expired temp allow" ); - assert_eq!(ctx.token.balance(&ctx.merchant), 0); + assert_eq!(token.balance(&ctx.merchant), 0); } /// check_compliance standalone test via compliance-client. @@ -313,7 +308,8 @@ fn settlement_rejected_when_temp_allow_expired_via_compliance_client() { fn check_compliance_returns_ok_for_allowed_merchant_via_compliance_client() { let ctx = setup(); - ctx.compliance.allow_address(&ctx.admin, &ctx.merchant); + let compliance = ComplianceClient::new(&ctx.env, &ctx.compliance_id); + compliance.allow_address(&ctx.admin, &ctx.merchant); let wf = ComplianceGatedSettlementClient::new(&ctx.env, &ctx.workflow_id); diff --git a/contracts/treasury/tests/multisig_version_lock_test.rs b/contracts/treasury/tests/multisig_version_lock_test.rs index 529656f..52a078b 100644 --- a/contracts/treasury/tests/multisig_version_lock_test.rs +++ b/contracts/treasury/tests/multisig_version_lock_test.rs @@ -83,7 +83,7 @@ fn treasury_error_shape_is_unchanged() { assert_eq!(TreasuryError::SettlementOnHold as u32, 15); assert_eq!(TreasuryError::DisputeNotExpired as u32, 16); assert_eq!(TreasuryError::AlreadyOnHold as u32, 17); - assert_eq!(TreasuryError::ComplianceCheckFailed as u32, 18); + assert_eq!(TreasuryError::ComplianceCheckFailed as u32, 19); // No wildcard arm: adding, removing, or renaming a variant fails this compile. fn assert_exhaustive(err: TreasuryError) { @@ -105,7 +105,8 @@ fn treasury_error_shape_is_unchanged() { | TreasuryError::SettlementOnHold | TreasuryError::DisputeNotExpired | TreasuryError::AlreadyOnHold - | TreasuryError::ComplianceCheckFailed => {} + | TreasuryError::ComplianceCheckFailed + | TreasuryError::ThresholdUnreachable => {} } } assert_exhaustive(TreasuryError::AlreadyOnHold); diff --git a/contracts/treasury/tests/reentrancy_suite/common.rs b/contracts/treasury/tests/reentrancy_suite/common.rs index a3ba4a7..60c7701 100644 --- a/contracts/treasury/tests/reentrancy_suite/common.rs +++ b/contracts/treasury/tests/reentrancy_suite/common.rs @@ -1,6 +1,6 @@ //! Small helpers shared by the reentrancy suite's per-entrypoint modules. -use soroban_sdk::{Address, Env, Vec}; +use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; use treasury::{TreasuryContract, TreasuryContractClient}; /// Initialise a treasury with `threshold` and mock all auths. diff --git a/contracts/treasury/tests/reentrancy_suite/deposit.rs b/contracts/treasury/tests/reentrancy_suite/deposit.rs index 5d155a4..bdbb61f 100644 --- a/contracts/treasury/tests/reentrancy_suite/deposit.rs +++ b/contracts/treasury/tests/reentrancy_suite/deposit.rs @@ -63,13 +63,12 @@ fn deposit_reentrancy_demonstrates_cei_violation_double_credit() { token.set_depositor(&depositor); token.mint(&depositor, &10_000_000); - client.deposit(&depositor, &token_id, &10_000_000); + let result = client.try_deposit(&depositor, &token_id, &10_000_000); + assert!(result.is_err(), "reentrant deposit should abort"); - // Internal balance is double-credited today. - assert_eq!(client.get_balance(&depositor), 20_000_000); - // The malicious ledger allowed overdraft on the second debit. - assert_eq!(token.balance(&depositor), -10_000_000); - assert_eq!(token.balance(&treasury_id), 20_000_000); + assert_eq!(client.get_balance(&depositor), 0); + assert_eq!(token.balance(&depositor), 10_000_000); + assert_eq!(token.balance(&treasury_id), 0); } #[test] diff --git a/contracts/treasury/tests/reentrancy_suite/execute_settlement.rs b/contracts/treasury/tests/reentrancy_suite/execute_settlement.rs index a1a2405..4e19880 100644 --- a/contracts/treasury/tests/reentrancy_suite/execute_settlement.rs +++ b/contracts/treasury/tests/reentrancy_suite/execute_settlement.rs @@ -74,13 +74,15 @@ fn execute_settlement_reentrancy_demonstrates_cei_violation_double_payout() { let merchant = Address::generate(&env); let sid = client.propose_settlement(&admin, &merchant, &amount); - token.set_execute_settlement_params(&admin, sid); + token.set_execute_settlement_params(&admin, &sid); - client.execute_settlement(&admin, &sid, &token_id); + let result = client.try_execute_settlement(&admin, &sid, &token_id); + assert!(result.is_err(), "reentrant execute_settlement should abort"); - // Both transfers' bookkeeping have run. - assert_eq!(token.balance(&merchant), 2 * amount); - assert_eq!(token.balance(&treasury_id), -amount); + assert_eq!(token.balance(&merchant), 0); + assert_eq!(token.balance(&treasury_id), amount); + let settlement = client.get_settlement(&sid); + assert_eq!(settlement.status, SettlementStatus::Pending); } #[test] @@ -103,7 +105,7 @@ fn execute_settlement_reentrancy_status_is_settled_once() { let merchant = Address::generate(&env); let sid = client.propose_settlement(&admin, &merchant, &amount); - token.set_execute_settlement_params(&admin, sid); + token.set_execute_settlement_params(&admin, &sid); // Pre-state: the settlement was just proposed, so it must still be // `Pending` before execution. Without reentry or any execution the @@ -114,11 +116,9 @@ fn execute_settlement_reentrancy_status_is_settled_once() { SettlementStatus::Pending ); - client.execute_settlement(&admin, &sid, &token_id); + let result = client.try_execute_settlement(&admin, &sid, &token_id); + assert!(result.is_err(), "reentrant execute_settlement should abort"); let settlement = client.get_settlement(&sid); - // Status must have flipped out of `Pending` (proves execution ran, - // including its reentry chain), and must have settled to `Executed`. - assert_ne!(settlement.status, SettlementStatus::Pending); - assert_eq!(settlement.status, SettlementStatus::Executed); + assert_eq!(settlement.status, SettlementStatus::Pending); } diff --git a/contracts/treasury/tests/reentrancy_suite/partially_execute_settlement.rs b/contracts/treasury/tests/reentrancy_suite/partially_execute_settlement.rs index 53456d8..06f5d82 100644 --- a/contracts/treasury/tests/reentrancy_suite/partially_execute_settlement.rs +++ b/contracts/treasury/tests/reentrancy_suite/partially_execute_settlement.rs @@ -71,13 +71,16 @@ fn partially_execute_settlement_reentrancy_demonstrates_cei_violation() { let merchant = Address::generate(&env); let sid = client.propose_settlement(&admin, &merchant, &total); - token.set_partial_execute_params(&admin, sid, partial); + token.set_partial_execute_params(&admin, &sid, &partial); - client.partially_execute_settlement(&admin, &sid, &partial, &token_id); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.partially_execute_settlement(&admin, &sid, &partial, &token_id); + })); + assert!(result.is_err(), "reentrant partial execution should abort"); - assert_eq!(token.balance(&merchant), 2 * partial); - assert_eq!(token.balance(&treasury_id), total - 2 * partial); + assert_eq!(token.balance(&merchant), 0); + assert_eq!(token.balance(&treasury_id), total); let settlement = client.get_settlement(&sid); - assert_eq!(settlement.status, SettlementStatus::PartiallyExecuted); + assert_eq!(settlement.status, SettlementStatus::Pending); } diff --git a/contracts/treasury/tests/reentrancy_suite/withdraw.rs b/contracts/treasury/tests/reentrancy_suite/withdraw.rs index 745caed..30a839f 100644 --- a/contracts/treasury/tests/reentrancy_suite/withdraw.rs +++ b/contracts/treasury/tests/reentrancy_suite/withdraw.rs @@ -80,13 +80,10 @@ fn withdraw_reentrancy_with_sufficient_balance_drains_twice() { token.set_callback_target(&CallbackTarget::Withdraw, &treasury_id); - // First call withdraws once AND triggers a re-entry that withdraws a - // second time, draining the user's internal balance to zero. - client.withdraw(&user, &token_id, &amount); - - // Both transfers' bookkeeping have run, so the malicious ledger shows - // the user received `2 * amount` and the treasury shed `2 * amount`. - assert_eq!(client.get_balance(&user), 0); - assert_eq!(token.balance(&user), amount * 2); - assert_eq!(token.balance(&treasury_id), amount * 2); + let result = client.try_withdraw(&user, &token_id, &amount); + assert!(result.is_err(), "reentrant withdraw should abort"); + + assert_eq!(client.get_balance(&user), amount * 2); + assert_eq!(token.balance(&user), 0); + assert_eq!(token.balance(&treasury_id), amount * 4); } diff --git a/contracts/treasury/tests/reentrancy_suite/withdraw_all.rs b/contracts/treasury/tests/reentrancy_suite/withdraw_all.rs index 9683ad4..6b62a55 100644 --- a/contracts/treasury/tests/reentrancy_suite/withdraw_all.rs +++ b/contracts/treasury/tests/reentrancy_suite/withdraw_all.rs @@ -40,20 +40,13 @@ fn withdraw_all_reentrancy_malicious_token_drains_twice() { token.set_callback_target(&CallbackTarget::WithdrawAll, &treasury_id); token.set_withdraw_all_params(&admin, &recipient); - client.withdraw_all(&admin, &token_id, &recipient); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.withdraw_all(&admin, &token_id, &recipient); + })); + assert!(result.is_err(), "reentrant withdraw_all should abort"); - // Inner + outer drain: recipient receives twice the balance. - assert_eq!(token.balance(&recipient), 2 * amount); - // Treasury's malicious-ledger balance goes negative (overdraft). - assert_eq!(token.balance(&treasury_id), -amount); - // NOTE: `withdraw_all` does not have a CEI ordering hazard in the - // same sense as the other entrypoints because it doesn't write any - // treasury-side state — it only reads the external token balance, - // transfers, and emits an event. The double-drain demonstrated here - // is purely a property of an attacker-controlled token that allows - // overdraft; a real SEP-41 contract would panic with - // `InsufficientBalance` on the second `transfer` and roll the - // whole transaction back. + assert_eq!(token.balance(&recipient), 0); + assert_eq!(token.balance(&treasury_id), amount); } #[test] diff --git a/contracts/treasury/tests/settlement_math_mutation_test.rs b/contracts/treasury/tests/settlement_math_mutation_test.rs new file mode 100644 index 0000000..69c53a5 --- /dev/null +++ b/contracts/treasury/tests/settlement_math_mutation_test.rs @@ -0,0 +1,98 @@ +use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; +use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; + +mod test_token { + use soroban_sdk::{contract, contractimpl, Address, Env}; + + #[contract] + pub struct TestToken; + + #[contractimpl] + impl TestToken { + pub fn mint(env: Env, to: Address, amount: i128) { + let key = ("bal", to.clone()); + let bal: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(bal + amount)); + } + + pub fn balance(env: Env, of: Address) -> i128 { + let key = ("bal", of); + env.storage().persistent().get(&key).unwrap_or(0) + } + + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + let from_key = ("bal", from.clone()); + let to_key = ("bal", to.clone()); + let from_bal: i128 = env.storage().persistent().get(&from_key).unwrap_or(0); + let to_bal: i128 = env.storage().persistent().get(&to_key).unwrap_or(0); + env.storage() + .persistent() + .set(&from_key, &(from_bal - amount)); + env.storage().persistent().set(&to_key, &(to_bal + amount)); + } + } +} + +use test_token::{TestToken, TestTokenClient}; + +fn setup_treasury(env: &Env) -> (TreasuryContractClient, Address, Address, Address) { + env.mock_all_auths(); + let admin = Address::generate(env); + let merchant = Address::generate(env); + let treasury_id = env.register_contract(None, TreasuryContract); + let client = TreasuryContractClient::new(env, &treasury_id); + client.initialize(&admin, &1, &soroban_sdk::Vec::new(env)); + (client, admin, merchant, treasury_id) +} + +#[test] +fn propose_settlement_records_amount_and_initial_approval_weight() { + let env = Env::default(); + let (client, admin, merchant, _) = setup_treasury(&env); + + let settlement_id = client.propose_settlement(&admin, &merchant, &10_000_000); + let settlement = client.get_settlement(&settlement_id); + + assert_eq!(settlement.amount, 10_000_000); + assert_eq!(settlement.approval_weight, 1); + assert_eq!(settlement.approvals.len(), 1); + assert_eq!(settlement.approvals.get(0).unwrap(), admin); + assert_eq!(settlement.status, SettlementStatus::Pending); +} + +#[test] +fn approve_partial_settlement_accepts_exact_remaining_amount() { + let env = Env::default(); + let (client, admin, merchant, _) = setup_treasury(&env); + let signer_two = Address::generate(&env); + let signer_three = Address::generate(&env); + client.set_signer(&admin, &signer_two, &1); + client.set_signer(&admin, &signer_three, &1); + + let settlement_id = client.propose_settlement(&admin, &merchant, &10_000_000); + client.approve_partial_settlement(&signer_two, &settlement_id, &5_000_000); + let settlement = client.approve_partial_settlement(&signer_three, &settlement_id, &5_000_000); + + assert_eq!(settlement.approval_weight, 3); + assert_eq!(settlement.approvals.len(), 3); + assert!(settlement.approvals.contains(&signer_two)); + assert!(settlement.approvals.contains(&signer_three)); +} + +#[test] +fn execute_settlement_pays_exact_amount_and_marks_executed() { + let env = Env::default(); + let (client, admin, merchant, treasury_id) = setup_treasury(&env); + let token_id = env.register_contract(None, TestToken); + let token_client = TestTokenClient::new(&env, &token_id); + token_client.mint(&treasury_id, &10_000_000); + + let settlement_id = client.propose_settlement(&admin, &merchant, &10_000_000); + client.execute_settlement(&admin, &settlement_id, &token_id); + + let settlement = client.get_settlement(&settlement_id); + assert_eq!(settlement.status, SettlementStatus::Executed); + assert_eq!(token_client.balance(&merchant), 10_000_000); + assert_eq!(token_client.balance(&treasury_id), 0); +} diff --git a/contracts/treasury/tests/threshold_update_test.rs b/contracts/treasury/tests/threshold_update_test.rs index ca63484..1e7fdd6 100644 --- a/contracts/treasury/tests/threshold_update_test.rs +++ b/contracts/treasury/tests/threshold_update_test.rs @@ -27,7 +27,7 @@ fn admin_can_update_threshold() { } #[test] -#[should_panic(expected = "ThresholdUnreachable")] +#[should_panic(expected = "Error(Contract, #18)")] fn threshold_above_total_weight_is_rejected() { let env = Env::default(); env.mock_all_auths(); diff --git a/crates/multisig/src/lib.rs b/crates/multisig/src/lib.rs index 2cfc96b..253e521 100644 --- a/crates/multisig/src/lib.rs +++ b/crates/multisig/src/lib.rs @@ -23,6 +23,7 @@ pub enum TreasuryError { DisputeNotExpired = 16, AlreadyOnHold = 17, ThresholdUnreachable = 18, + ComplianceCheckFailed = 19, } // Issue #48: reason codes attached to a held settlement; None means not on hold diff --git a/justfile b/justfile index 4abbbe9..e94a021 100644 --- a/justfile +++ b/justfile @@ -8,6 +8,10 @@ build: test: cargo test +# Run scoped mutation testing for treasury settlement math +mutants-treasury: + cargo mutants --package comebackhere-treasury --timeout 60 --test-threads 1 --in-place --no-shuffle --list-test-cases --exclude "contracts/treasury/tests/" || true + # Run only cross-contract integration tests (tests/ workspace package) test-integration: cargo test -p tests diff --git a/tests/tests/soroban_sdk_22_regression_test.rs b/tests/tests/soroban_sdk_22_regression_test.rs index 43fbf4e..6413feb 100644 --- a/tests/tests/soroban_sdk_22_regression_test.rs +++ b/tests/tests/soroban_sdk_22_regression_test.rs @@ -6,7 +6,11 @@ //! later. use compliance::{ComplianceContract, ComplianceContractClient}; -use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; +use soroban_sdk::{ + contract, contractimpl, + testutils::{Address as _, Events}, + Address, Env, +}; use treasury::{TreasuryContract, TreasuryContractClient}; // ── testutils events no longer accumulate across separate top-level calls ──