diff --git a/.gitignore b/.gitignore index e071d56c..f559abb8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,7 @@ Thumbs.db PR_PASSWORD_RESET.md .agent/ -.agents/ issue.md # python env -myenv/ \ No newline at end of file +myenv/ diff --git a/apps/onchain/Cargo.lock b/apps/onchain/Cargo.lock index 835188ae..9199c2e2 100644 --- a/apps/onchain/Cargo.lock +++ b/apps/onchain/Cargo.lock @@ -23,6 +23,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arbitrary" version = "1.3.2" @@ -147,7 +153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -174,6 +180,27 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[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 = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "block-buffer" version = "0.10.4" @@ -290,6 +317,7 @@ dependencies = [ name = "crowdfund_vault" version = "0.0.0" dependencies = [ + "proptest", "notification_interface", "soroban-sdk", ] @@ -301,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -547,7 +575,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -572,7 +600,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -584,6 +612,16 @@ 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", +] + [[package]] name = "escape-bytes" version = "0.1.1" @@ -596,13 +634,19 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -624,6 +668,12 @@ 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 = "generic-array" version = "0.14.9" @@ -648,6 +698,31 @@ dependencies = [ "wasm-bindgen", ] +[[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 = "group" version = "0.13.0" @@ -655,7 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -683,6 +758,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -753,6 +837,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -834,6 +924,12 @@ dependencies = [ "cpufeatures", ] +[[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.182" @@ -846,6 +942,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.29" @@ -1005,6 +1107,31 @@ 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.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[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.44" @@ -1014,6 +1141,18 @@ 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 = "rand" version = "0.8.5" @@ -1021,8 +1160,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1032,7 +1181,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "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]] @@ -1041,7 +1200,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "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]] @@ -1064,6 +1241,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rfc6979" version = "0.4.0" @@ -1083,12 +1266,37 @@ dependencies = [ "semver", ] +[[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", + "windows-sys", +] + [[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 = "schemars" version = "0.8.22" @@ -1252,7 +1460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1289,7 +1497,7 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-xdr", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -1317,7 +1525,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom", + "getrandom 0.2.17", "hex-literal", "hmac", "k256", @@ -1325,8 +1533,8 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "sec1", "sha2", "sha3", @@ -1335,7 +1543,7 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-strkey 0.0.13", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -1379,7 +1587,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand", + "rand 0.8.5", "rustc_version", "serde", "serde_json", @@ -1420,7 +1628,7 @@ dependencies = [ "base64", "stellar-xdr", "thiserror", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -1554,6 +1762,19 @@ dependencies = [ "unicode-ident", ] +[[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", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1611,12 +1832,24 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[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 = "upgradable-contract" version = "0.1.0" @@ -1648,12 +1881,39 @@ dependencies = [ "syn 2.0.117", ] +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -1699,6 +1959,28 @@ 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 2.13.0", + "wasm-encoder", + "wasmparser 0.244.0", +] + [[package]] name = "wasmi_arena" version = "0.4.1" @@ -1727,6 +2009,18 @@ dependencies = [ "semver", ] +[[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 2.13.0", + "semver", +] + [[package]] name = "wasmparser-nostd" version = "0.100.2" @@ -1795,6 +2089,103 @@ dependencies = [ "windows-link", ] +[[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 = "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-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 2.13.0", + "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 2.13.0", + "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 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + [[package]] name = "zerocopy" version = "0.8.39" diff --git a/apps/onchain/contracts/crowdfund_vault/Cargo.toml b/apps/onchain/contracts/crowdfund_vault/Cargo.toml index 68648c5d..4ddad353 100644 --- a/apps/onchain/contracts/crowdfund_vault/Cargo.toml +++ b/apps/onchain/contracts/crowdfund_vault/Cargo.toml @@ -14,6 +14,7 @@ notification_interface = { path = "../notification_interface" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } +proptest = "1" [features] testutils = ["soroban-sdk/testutils"] diff --git a/apps/onchain/contracts/crowdfund_vault/src/lib.rs b/apps/onchain/contracts/crowdfund_vault/src/lib.rs index 8615c56a..ebf319b0 100644 --- a/apps/onchain/contracts/crowdfund_vault/src/lib.rs +++ b/apps/onchain/contracts/crowdfund_vault/src/lib.rs @@ -7,7 +7,7 @@ mod storage; mod token; mod yield_provider; -use errors::CrowdfundError; +pub use errors::CrowdfundError; use math::{sqrt_scaled, unscale}; use notification_interface::{Notification, NotificationReceiverClient}; use soroban_sdk::token::TokenClient; diff --git a/apps/onchain/contracts/crowdfund_vault/tests/invariants.rs b/apps/onchain/contracts/crowdfund_vault/tests/invariants.rs new file mode 100644 index 00000000..b61eca82 --- /dev/null +++ b/apps/onchain/contracts/crowdfund_vault/tests/invariants.rs @@ -0,0 +1,293 @@ +// Invariant test suite for crowdfund_vault +// Feature: invariant-hardening +// +// Each property test references the requirement it validates via a comment. +// All properties run 1000 cases (ProptestConfig::with_cases(1000)). + +use crowdfund_vault::CrowdfundVaultContractClient; +use proptest::prelude::*; +use soroban_sdk::{ + symbol_short, + testutils::Address as _, + token::{StellarAssetClient, TokenClient}, + Address, Env, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// All handles needed to interact with a freshly-initialised contract. +struct TestContext<'a> { + env: Env, + client: CrowdfundVaultContractClient<'a>, + admin: Address, + owner: Address, + token: TokenClient<'a>, + token_admin: StellarAssetClient<'a>, + project_id: u64, +} + +/// Create a fresh Soroban Env, register the contract, initialise it, mint +/// tokens to `owner`, and create one project. Returns a `TestContext` with +/// all handles wired up. +fn setup_env<'a>(env: &'a Env) -> TestContext<'a> { + env.mock_all_auths(); + + let admin = Address::generate(env); + let owner = Address::generate(env); + + // Register a Stellar asset contract so we have a real token. + let asset_contract = env.register_stellar_asset_contract_v2(admin.clone()); + let token = TokenClient::new(env, &asset_contract.address()); + let token_admin = StellarAssetClient::new(env, &asset_contract.address()); + + // Mint a generous supply to the owner so deposits can succeed. + token_admin.mint(&owner, &100_000_000); + + // Register and initialise the vault contract. + let contract_id = env.register(crowdfund_vault::CrowdfundVaultContract, ()); + let client = CrowdfundVaultContractClient::new(env, &contract_id); + client.initialize(&admin); + + // Create one project owned by `owner`. + let project_id = client.create_project( + &owner, + &symbol_short!("TestProj"), + &1_000_000, + &token.address, + ); + + TestContext { + env: env.clone(), + client, + admin, + owner, + token, + token_admin, + project_id, + } +} + +// --------------------------------------------------------------------------- +// Placeholder — properties will be added in subsequent tasks +// --------------------------------------------------------------------------- + +#[test] +fn scaffold_compiles() { + let env = Env::default(); + let _ctx = setup_env(&env); + // No assertions — just verifies the scaffold compiles and setup_env works. +} + +// --------------------------------------------------------------------------- +// Balance Conservation — Requirement 1 +// --------------------------------------------------------------------------- + +// Feature: invariant-hardening, Property 1: Balance equals sum of contributions +// Validates: Requirements 1.1, 1.5 +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn prop_balance_equals_sum_of_contributions( + amounts in proptest::collection::vec(1i128..=1_000_000i128, 1..=5usize) + ) { + let env = Env::default(); + env.mock_all_auths(); + let ctx = setup_env(&env); + + // Generate a distinct contributor address for each amount and deposit. + for amount in &amounts { + let contributor = Address::generate(&env); + // Mint enough tokens to this contributor. + ctx.token_admin.mint(&contributor, amount); + ctx.client.deposit(&contributor, &ctx.project_id, amount); + } + + let expected_sum: i128 = amounts.iter().sum(); + + // ProjectBalance must equal the sum of all deposits. + let balance = ctx.client.get_balance(&ctx.project_id); + prop_assert_eq!(balance, expected_sum); + + // total_deposited on ProjectData must also equal the sum. + let project = ctx.client.get_project(&ctx.project_id); + prop_assert_eq!(project.total_deposited, expected_sum); + } +} + +// --------------------------------------------------------------------------- +// Withdrawal Safety — Requirement 3 +// --------------------------------------------------------------------------- + +// Feature: invariant-hardening, Property 7: Overdraft is rejected +// Validates: Requirements 3.1 +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn prop_overdraft( + deposit_amount in 1i128..=1_000_000i128, + excess in 1i128..=1_000_000i128, + ) { + let env = Env::default(); + env.mock_all_auths(); + let ctx = setup_env(&env); + + // Deposit D tokens from the owner into the project. + ctx.token_admin.mint(&ctx.owner, &deposit_amount); + ctx.client.deposit(&ctx.owner, &ctx.project_id, &deposit_amount); + + // Approve the milestone so the withdrawal gate is open. + ctx.client.approve_milestone(&ctx.admin, &ctx.project_id); + + // Attempt to withdraw W = D + excess, which exceeds the balance. + let withdraw_amount = deposit_amount + excess; + let result = ctx.client.try_withdraw(&ctx.project_id, &withdraw_amount); + + // Must be rejected with InsufficientBalance. + prop_assert!( + result.is_err(), + "expected InsufficientBalance error but got Ok" + ); + + // Balance must remain unchanged at D. + let balance = ctx.client.get_balance(&ctx.project_id); + prop_assert_eq!(balance, deposit_amount); + } +} + +// --------------------------------------------------------------------------- +// Quadratic Funding Math — Requirement 4 +// --------------------------------------------------------------------------- + +// Feature: invariant-hardening, Property 9: Single-contributor match approximation +// Validates: Requirements 4.2 +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn prop_single_contributor_match(c in 1i128..=10_000_000i128) { + let env = Env::default(); + env.mock_all_auths(); + let ctx = setup_env(&env); + + let contributor = Address::generate(&env); + ctx.token_admin.mint(&contributor, &c); + ctx.client.deposit(&contributor, &ctx.project_id, &c); + + let match_amount = ctx.client.calculate_match(&ctx.project_id); + + // For a single contributor with amount C, the quadratic formula gives + // (sqrt(C))^2 = C, so the result should be approximately C within 1% + // tolerance for fixed-point rounding in sqrt_scaled. + let lower = c * 99 / 100; + let upper = c * 101 / 100; + + prop_assert!( + match_amount >= lower && match_amount <= upper, + "calculate_match({}) = {} is outside [{}, {}]", + c, match_amount, lower, upper + ); + } +} + +// --------------------------------------------------------------------------- +// Pause Invariants — Requirement 5 +// --------------------------------------------------------------------------- + +// Feature: invariant-hardening, Property 11: Paused contract blocks all mutations +// Validates: Requirements 5.1, 5.2, 5.3 +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn prop_paused_blocks_mutations( + deposit_amount in 1i128..=1_000_000i128, + target_amount in 1i128..=1_000_000i128, + ) { + let env = Env::default(); + env.mock_all_auths(); + let ctx = setup_env(&env); + + // Pause the contract. + ctx.client.pause(&ctx.admin); + + // --- try_deposit must return ContractPaused --- + let contributor = Address::generate(&env); + ctx.token_admin.mint(&contributor, &deposit_amount); + let deposit_result = ctx.client.try_deposit(&contributor, &ctx.project_id, &deposit_amount); + prop_assert!( + matches!( + deposit_result, + Err(Ok(crowdfund_vault::CrowdfundError::ContractPaused)) + ), + "try_deposit while paused should return ContractPaused, got {:?}", + deposit_result + ); + + // Balance must remain 0 (no state change). + let balance = ctx.client.get_balance(&ctx.project_id); + prop_assert_eq!(balance, 0i128, "balance should remain 0 after rejected deposit"); + + // --- try_create_project must return ContractPaused --- + let new_owner = Address::generate(&env); + let create_result = ctx.client.try_create_project( + &new_owner, + &symbol_short!("NewProj"), + &target_amount, + &ctx.token.address, + ); + prop_assert!( + matches!( + create_result, + Err(Ok(crowdfund_vault::CrowdfundError::ContractPaused)) + ), + "try_create_project while paused should return ContractPaused, got {:?}", + create_result + ); + + // --- try_withdraw must return ContractPaused --- + // (milestone approval also blocked while paused, but withdraw checks pause first) + let withdraw_result = ctx.client.try_withdraw(&ctx.project_id, &1i128); + prop_assert!( + matches!( + withdraw_result, + Err(Ok(crowdfund_vault::CrowdfundError::ContractPaused)) + ), + "try_withdraw while paused should return ContractPaused, got {:?}", + withdraw_result + ); + } +} + +// --------------------------------------------------------------------------- +// Deposit Integrity — Requirement 2 +// --------------------------------------------------------------------------- + +// Feature: invariant-hardening, Property 4: Contribution tracking round-trip +// Validates: Requirements 2.1 +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn prop_contribution_tracking(amount in 1i128..=10_000_000i128) { + let env = Env::default(); + env.mock_all_auths(); + let ctx = setup_env(&env); + + let contributor = Address::generate(&env); + + // Record prior contribution (should be 0 for a fresh contributor). + let prior = ctx.client.get_contribution(&ctx.project_id, &contributor); + + // Mint enough tokens and deposit. + ctx.token_admin.mint(&contributor, &amount); + ctx.client.deposit(&contributor, &ctx.project_id, &amount); + + // After deposit, contribution must equal prior + amount. + let after = ctx.client.get_contribution(&ctx.project_id, &contributor); + prop_assert_eq!(after, prior + amount); + } +}