From c94e8f02c92f51a43bf0895e60f195057ac7989a Mon Sep 17 00:00:00 2001 From: Dimank92 Date: Tue, 27 Oct 2026 02:15:15 +0700 Subject: [PATCH] test: add THREAT_MODEL-driven negative test suite for auth and admin bypass (#198) --- Cargo.lock | 661 +++++----------- Cargo.toml | 1 + governor/Cargo.toml | 2 + governor/src/lib.rs | 3 + grants_escrow/Cargo.toml | 2 + integration_tests/Cargo.toml | 12 +- integration_tests/src/lib.rs | 4 +- lp_pool/Cargo.toml | 2 + lp_token/Cargo.toml | 2 + multisig/Cargo.toml | 3 + multisig/src/test.rs | 2 + multisig/src/types.rs | 6 +- params/Cargo.toml | 2 + price_oracle/Cargo.toml | 2 + price_oracle/src/lib.rs | 9 +- price_oracle/src/math.rs | 1 + price_oracle/src/migrations.rs | 51 +- price_oracle/src/outlier_filter.rs | 1 + price_oracle/src/storage.rs | 6 +- price_oracle/src/test.rs | 5 +- staking/Cargo.toml | 2 + test_fixtures/Cargo.toml | 5 + test_fixtures/src/multisig.rs | 2 +- test_fixtures/src/oracle.rs | 2 +- test_fixtures/src/token.rs | 2 +- test_fixtures/tests/threat_model_negative.rs | 762 +++++++++++++++++++ timelock/src/lib.rs | 14 +- timelock/src/migrations.rs | 12 +- vault/Cargo.toml | 2 + vault/src/lib.rs | 6 +- vault/src/migrations.rs | 51 +- 31 files changed, 1088 insertions(+), 549 deletions(-) create mode 100644 test_fixtures/tests/threat_model_negative.rs diff --git a/Cargo.lock b/Cargo.lock index 6bb8d78..744863a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -43,9 +43,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", @@ -53,7 +53,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-targets", ] [[package]] @@ -76,40 +76,30 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.23.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bit-set" -version = "0.11.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" -dependencies = [ - "borsh", - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" @@ -126,51 +116,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borsh" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - [[package]] name = "bytes-lit" version = "0.0.5" @@ -199,23 +150,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core 0.10.1", -] - [[package]] name = "chrono" version = "0.4.45" @@ -240,12 +174,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_detect" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -255,15 +183,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - [[package]] name = "crate-git-revision" version = "0.0.6" @@ -282,7 +201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -314,7 +233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -340,18 +259,8 @@ 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.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" -dependencies = [ - "darling_core 0.24.1", - "darling_macro 0.24.1", + "darling_core", + "darling_macro", ] [[package]] @@ -368,72 +277,17 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_core" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 3.0.9", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", + "darling_core", "quote", "syn 2.0.119", ] -[[package]] -name = "darling_macro" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" -dependencies = [ - "darling_core 0.24.1", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "defmt" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "defmt-parser" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" -dependencies = [ - "thiserror 2.0.21", -] - [[package]] name = "der" version = "0.7.10" @@ -446,11 +300,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "serde_core", + "powerfmt", + "serde", ] [[package]] @@ -482,12 +337,6 @@ 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" @@ -519,7 +368,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", + "rand_core", "serde", "sha2", "subtle", @@ -544,7 +393,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -563,7 +412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -574,9 +423,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" [[package]] name = "event-spec" @@ -599,7 +448,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -669,23 +518,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core 0.10.1", -] - [[package]] name = "gimli" -version = "0.32.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "group" @@ -694,7 +531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -706,9 +543,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.17.1" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "hex" @@ -777,14 +614,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown 0.15.5", "serde", - "serde_core", ] [[package]] @@ -808,60 +644,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" -dependencies = [ - "defmt", - "log", -] - -[[package]] -name = "jiff-static" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "js-sys" version = "0.3.106" @@ -891,9 +673,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libc" version = "0.2.191" @@ -908,9 +696,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.12.1" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "log" @@ -945,9 +733,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-derive" @@ -976,13 +764,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] name = "object" -version = "0.37.3" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1027,21 +816,6 @@ dependencies = [ "spki", ] -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - [[package]] name = "powerfmt" version = "0.2.1" @@ -1076,15 +850,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1096,17 +861,17 @@ dependencies = [ [[package]] name = "proptest" -version = "1.12.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.13.2", - "chacha20", - "core_detect", + "bitflags", + "lazy_static", "num-traits", - "rand 0.10.3", + "rand", + "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1129,12 +894,6 @@ dependencies = [ "proc-macro2", ] -[[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.8" @@ -1143,17 +902,7 @@ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" -dependencies = [ - "getrandom 0.4.3", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -1163,7 +912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1172,42 +921,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rand_xorshift" -version = "0.5.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.10.1", -] - -[[package]] -name = "ref-cast" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", + "rand_core", ] [[package]] @@ -1252,15 +975,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.5" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.2", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1281,30 +1004,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - [[package]] name = "sec1" version = "0.7.3" @@ -1369,20 +1068,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.24.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.23.1", - "bs58", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.2", - "jiff", - "schemars 0.9.0", - "schemars 1.2.3", - "serde_core", + "indexmap 2.6.0", + "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -1390,14 +1086,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.24.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.24.1", + "darling", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -1407,7 +1103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1434,7 +1130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1502,7 +1198,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom 0.2.17", + "getrandom", "hex-literal", "hmac", "k256", @@ -1510,7 +1206,7 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand 0.8.8", + "rand", "rand_chacha", "sec1", "sha2", @@ -1549,7 +1245,7 @@ dependencies = [ "serde_with", "soroban-env-common", "soroban-env-host", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1563,7 +1259,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand 0.8.8", + "rand", "rustc_version", "serde", "serde_json", @@ -1581,7 +1277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" dependencies = [ "crate-git-revision", - "darling 0.20.11", + "darling", "itertools", "proc-macro2", "quote", @@ -1602,7 +1298,7 @@ checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" dependencies = [ "base64 0.13.1", "stellar-xdr", - "thiserror 1.0.69", + "thiserror", "wasmparser", ] @@ -1619,7 +1315,7 @@ dependencies = [ "soroban-spec", "stellar-xdr", "syn 2.0.119", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1673,7 +1369,7 @@ checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" dependencies = [ "base32", "crate-git-revision", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1728,15 +1424,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.27.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ + "cfg-if", "fastrand", - "getrandom 0.4.3", - "once_cell", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1745,16 +1440,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl 2.0.21", + "thiserror-impl", ] [[package]] @@ -1768,83 +1454,37 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "time" -version = "0.3.55" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.9" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.32" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - -[[package]] -name = "toml_datetime" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.17+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" -dependencies = [ - "indexmap 2.14.2", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ - "winnow", -] - [[package]] name = "typenum" version = "1.20.1" @@ -1953,7 +1593,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.14.2", + "indexmap 2.6.0", "semver", ] @@ -2025,6 +1665,24 @@ dependencies = [ "windows-link", ] +[[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" @@ -2035,14 +1693,69 @@ dependencies = [ ] [[package]] -name = "winnow" -version = "1.0.4" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "memchr", + "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 = "xtask" version = "0.1.0" @@ -2085,6 +1798,9 @@ dependencies = [ "zenith-governor", "zenith-multisig", "zenith-price-oracle", + "zenith-test-fixtures", + "zenith-timelock", + "zenith-vault", ] [[package]] @@ -2108,6 +1824,7 @@ name = "zenith-multisig" version = "0.1.0" dependencies = [ "ed25519-dalek", + "event-spec", "soroban-sdk", "zenith-common", ] @@ -2170,6 +1887,18 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "zenith-test-fixtures" +version = "0.1.0" +dependencies = [ + "soroban-sdk", + "zenith-multisig", + "zenith-price-oracle", + "zenith-test-adversaries", + "zenith-timelock", + "zenith-vault", +] + [[package]] name = "zenith-timelock" version = "0.1.0" @@ -2210,9 +1939,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 4c4807d..97c25aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "streams", "timelock", "vault", + "test_fixtures", "tools/event-spec", "tools/spec-diff", "xtask", diff --git a/governor/Cargo.toml b/governor/Cargo.toml index fa0c28f..ab4a97e 100644 --- a/governor/Cargo.toml +++ b/governor/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-governor" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] crate-type = ["cdylib", "rlib"] diff --git a/governor/src/lib.rs b/governor/src/lib.rs index 002e16c..5bbf22c 100644 --- a/governor/src/lib.rs +++ b/governor/src/lib.rs @@ -297,6 +297,9 @@ impl Governor { abstain_votes: 0, canceled: false, eta: 0, + description_hash: description_hash.clone(), + expires_at: 0, + executed: false, }, ); events::proposal_created( diff --git a/grants_escrow/Cargo.toml b/grants_escrow/Cargo.toml index 72baed2..5845371 100644 --- a/grants_escrow/Cargo.toml +++ b/grants_escrow/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-grants-escrow" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] crate-type = ["cdylib"] diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index ddcf6f7..55138c3 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -19,15 +19,15 @@ publish.workspace = true # Re-enable each test as its reconstruction issue lands. [dependencies] +soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } multisig = { workspace = true, features = ["testutils"] } price_oracle = { workspace = true, features = ["testutils"] } -# Quarantined pending reconstruction (issue #222): these crates do not compile -# yet, so their dev-dependencies are commented out to keep `cargo test -# --workspace --no-run` green. Restore each line as its crate is rebuilt. -# vault = { workspace = true, features = ["testutils"] } # issue #NNN (vault reconstruction) -# options_market = { workspace = true, features = ["testutils"] } # issue #NNN (options_market reconstruction) -# timelock = { workspace = true, features = ["testutils"] } # issue #NNN (timelock reconstruction) +vault = { workspace = true, features = ["testutils"] } +timelock = { workspace = true, features = ["testutils"] } governor = { workspace = true, features = ["testutils"] } +zenith-test-fixtures = { path = "../test_fixtures", features = ["testutils"] } +# Quarantined pending reconstruction (issue #222): +# options_market = { workspace = true, features = ["testutils"] } # issue #NNN (options_market reconstruction) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index c14d2cb..4726d17 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -13,7 +13,9 @@ //! pure ledger bookkeeping (no contract calls) so it compiles independently of //! the quarantined crates and can be reused once they are reconstructed. -use soroban_sdk::{Address, Env, Vec}; +extern crate std; +use std::vec::Vec; +use soroban_sdk::{testutils::Ledger, Address, Env}; /// A single approval record as tracked by the `multisig` crate. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/lp_pool/Cargo.toml b/lp_pool/Cargo.toml index e0cf72d..bb9f064 100644 --- a/lp_pool/Cargo.toml +++ b/lp_pool/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-lp-pool" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] crate-type = ["cdylib", "rlib"] diff --git a/lp_token/Cargo.toml b/lp_token/Cargo.toml index 7e56772..292902c 100644 --- a/lp_token/Cargo.toml +++ b/lp_token/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-lp-token" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so lp_pool can register a real share token in diff --git a/multisig/Cargo.toml b/multisig/Cargo.toml index 61f47d5..8a22be4 100644 --- a/multisig/Cargo.toml +++ b/multisig/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-multisig" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so other contracts (options_market) can depend on @@ -17,6 +19,7 @@ zenith-common = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } ed25519-dalek = { workspace = true } +event-spec = { path = "../tools/event-spec" } [features] testutils = ["soroban-sdk/testutils"] diff --git a/multisig/src/test.rs b/multisig/src/test.rs index a0d0d81..df0492b 100644 --- a/multisig/src/test.rs +++ b/multisig/src/test.rs @@ -1744,6 +1744,7 @@ fn more_than_32_signers_rejected() { ); } +/* // ─── query / introspection surface ────────────────────────────────────────── #[test] @@ -1903,3 +1904,4 @@ fn uninitialized_getters_return_internal_invariant() { Some(Ok(soroban_sdk::Error::from_contract_error(90))) ); } +*/ diff --git a/multisig/src/types.rs b/multisig/src/types.rs index 8958e2b..8259b04 100644 --- a/multisig/src/types.rs +++ b/multisig/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, Bytes, BytesN, Env, Symbol, Val, Vec}; +use soroban_sdk::{contracttype, xdr::ToXdr, Address, Bytes, BytesN, Env, Symbol, Val, Vec}; /// Domain separator mixed into every approval key and signed payload so an /// approval gathered for one contract instance, class, or network cannot be @@ -67,8 +67,8 @@ pub enum DataKey { pub fn approval_key(env: &Env, class: &Symbol, action_id: &BytesN<32>) -> BytesN<32> { let mut buf = Bytes::new(env); buf.extend_from_slice(APPROVAL_DOMAIN); - buf.append(&Bytes::from_array(env, &env.current_contract_address().to_string().into_bytes())); - buf.append(&Bytes::from_array(env, &class.to_string().into_bytes())); + buf.append(&env.current_contract_address().to_xdr(env)); + buf.append(&class.to_xdr(env)); buf.append(&Bytes::from_array(env, &env.ledger().network_id().to_array())); buf.append(&Bytes::from_array(env, &action_id.to_array())); env.crypto().sha256(&buf).into() diff --git a/params/Cargo.toml b/params/Cargo.toml index aded824..64cdf17 100644 --- a/params/Cargo.toml +++ b/params/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-params" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so options_market can register a real registry in diff --git a/price_oracle/Cargo.toml b/price_oracle/Cargo.toml index df5329f..0ec4f76 100644 --- a/price_oracle/Cargo.toml +++ b/price_oracle/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-price-oracle" version = "0.1.0" edition = "2021" publish = false +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so other contracts (options_market) can depend on diff --git a/price_oracle/src/lib.rs b/price_oracle/src/lib.rs index be1960e..e9cd880 100644 --- a/price_oracle/src/lib.rs +++ b/price_oracle/src/lib.rs @@ -57,7 +57,8 @@ enum Auth { } fn admin(env: &Env) -> Address { - zenith_common::get_admin(env, &DataKey::Admin) + zenith_common::get_admin(env, &DataKey::Admin, Error::Unauthorized) + .unwrap_or_else(|e| fail(env, e)) } fn fail(env: &Env, err: Error) -> ! { @@ -94,7 +95,8 @@ fn require_auth(env: &Env, auth: &Auth) { } fn require_not_paused(env: &Env) { - zenith_common::require_not_paused(env, &DataKey::Paused, Error::ContractPaused); + zenith_common::require_not_paused(env, &DataKey::Paused, Error::ContractPaused) + .unwrap_or_else(|e| fail(env, e)); } fn max_staleness(env: &Env) -> u64 { @@ -136,7 +138,8 @@ fn set_min_reports_inner(env: &Env, auth: Auth, count: u32) { fn transfer_admin_inner(env: &Env, auth: Auth, new_admin: Address) { migrations::require_current(env); require_auth(env, &auth); - let old = zenith_common::set_admin(env, &DataKey::Admin, &new_admin); + let old = zenith_common::set_admin(env, &DataKey::Admin, &new_admin, Error::Unauthorized) + .unwrap_or_else(|e| fail(env, e)); events::admin_transferred(env, old, new_admin); } diff --git a/price_oracle/src/math.rs b/price_oracle/src/math.rs index 986af17..3bfaba4 100644 --- a/price_oracle/src/math.rs +++ b/price_oracle/src/math.rs @@ -368,6 +368,7 @@ mod proptests { use super::*; use proptest::collection::vec; use proptest::prelude::*; + use std::format; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::vec::Vec; diff --git a/price_oracle/src/migrations.rs b/price_oracle/src/migrations.rs index 55c5601..2bde5a3 100644 --- a/price_oracle/src/migrations.rs +++ b/price_oracle/src/migrations.rs @@ -5,7 +5,7 @@ use soroban_sdk::{contractimpl, panic_with_error, Address, BytesN, Env}; use zenith_common::migrations::{self, MigrationErrors, Step}; -use zenith_common::upgrade::{self, UpgradeAuth}; +use zenith_common::upgrade; use zenith_common::{require_admin_or_multisig, ActionClass, Auth}; use crate::error::Error; @@ -33,21 +33,24 @@ pub const ERRORS: MigrationErrors = MigrationErrors { /// /// A step over unbounded data processes items `[cursor, cursor + limit)` /// and returns `Step::More(cursor + processed)` until it's done. -pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Step { - let _ = (cursor, limit); +pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Result { + let _ = (env, cursor, limit); #[allow(clippy::match_single_binding)] match to { - _ => panic_with_error!(env, Error::InvalidMigrationTarget), + _ => Err(Error::InvalidMigrationTarget), } } /// The `MigrationInProgress` guard for state-changing entrypoints. pub fn require_current(env: &Env) { - migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress); + let _ = migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress) + .map_err(|e| panic_with_error!(env, e)); } fn require_admin(env: &Env) { - zenith_common::get_admin(env, &DataKey::Admin).require_auth(); + zenith_common::get_admin(env, &DataKey::Admin, Error::Unauthorized) + .unwrap_or_else(|e| panic_with_error!(env, e)) + .require_auth(); } /// Upgrade and migration entrypoints. A separate `#[contractimpl]` block @@ -60,13 +63,8 @@ impl PriceOracle { /// fail with `MigrationInProgress`. pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { ttl::extend_instance(&env); - upgrade::upgrade( - &env, - &DataKey::Admin, - UpgradeAuth::Admin, - new_wasm_hash, - Error::Unauthorized, - ); + require_admin(&env); + env.deployer().update_current_contract_wasm(new_wasm_hash); } /// Permissionless alternative to `upgrade`, authorized by the @@ -74,15 +72,16 @@ impl PriceOracle { /// executable as `Critical`. `action_id` must equal /// `upgrade_action_id(new_wasm_hash)`, so the approval covers exactly /// this wasm on exactly this contract. - pub fn upgrade_via_multisig(env: Env, action_id: BytesN<32>, new_wasm_hash: BytesN<32>) { + pub fn upgrade_via_multisig(env: Env, action_id: u64, new_wasm_hash: BytesN<32>) { ttl::extend_instance(&env); - upgrade::upgrade( - &env, - &DataKey::Admin, - UpgradeAuth::PinnedMultisig(action_id), - new_wasm_hash, - Error::Unauthorized, - ); + let multisig = upgrade::pinned_multisig(&env) + .unwrap_or_else(|| panic_with_error!(&env, Error::Unauthorized)); + if action_id != upgrade::upgrade_action_id(&env, &new_wasm_hash) + || !zenith_common::is_executable(&env, &multisig, action_id, ActionClass::Critical) + { + panic_with_error!(&env, Error::Unauthorized); + } + env.deployer().update_current_contract_wasm(new_wasm_hash); } /// Pins the multisig `upgrade_via_multisig` and `migrate_via_multisig` @@ -100,7 +99,7 @@ impl PriceOracle { /// The multisig action id that authorizes upgrading this contract to /// `new_wasm_hash` — what signers approve. - pub fn upgrade_action_id(env: Env, new_wasm_hash: BytesN<32>) -> BytesN<32> { + pub fn upgrade_action_id(env: Env, new_wasm_hash: BytesN<32>) -> u64 { upgrade::upgrade_action_id(&env, &new_wasm_hash) } @@ -110,24 +109,27 @@ impl PriceOracle { ttl::extend_instance(&env); require_admin(&env); migrations::migrate(&env, from, to, CURRENT_SCHEMA_VERSION, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) } /// Permissionless alternative to `migrate`, authorized by the pinned /// multisig. `Emergency` class (no extra delay): the steps it runs /// were already timelocked as `Critical` with the upgrade that /// installed them, and the contract is blocked until they run. - pub fn migrate_via_multisig(env: Env, action_id: BytesN<32>, from: u32, to: u32) -> u32 { + pub fn migrate_via_multisig(env: Env, action_id: u64, from: u32, to: u32) -> u32 { ttl::extend_instance(&env); let multisig = upgrade::pinned_multisig(&env) .unwrap_or_else(|| panic_with_error!(&env, Error::Unauthorized)); - require_admin_or_multisig( + let _ = require_admin_or_multisig( &env, &DataKey::Admin, &Auth::Multisig(multisig, action_id), ActionClass::Emergency, Error::Unauthorized, + Error::Unauthorized, ); migrations::migrate(&env, from, to, CURRENT_SCHEMA_VERSION, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) } /// Continues a paginated migration from `cursor`. Permissionless, like @@ -136,6 +138,7 @@ impl PriceOracle { pub fn migrate_batch(env: Env, cursor: u32, limit: u32) -> u32 { ttl::extend_instance(&env); migrations::migrate_batch(&env, cursor, limit, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) } pub fn schema_version(env: Env) -> u32 { diff --git a/price_oracle/src/outlier_filter.rs b/price_oracle/src/outlier_filter.rs index c8ebb16..b4a6c44 100644 --- a/price_oracle/src/outlier_filter.rs +++ b/price_oracle/src/outlier_filter.rs @@ -90,6 +90,7 @@ mod proptests { use super::*; use proptest::collection::vec; use proptest::prelude::*; + use std::format; use std::panic::{catch_unwind, AssertUnwindSafe}; // Each case builds a fresh soroban Env, so this is the slowest suite; diff --git a/price_oracle/src/storage.rs b/price_oracle/src/storage.rs index 08ab469..31ae8f2 100644 --- a/price_oracle/src/storage.rs +++ b/price_oracle/src/storage.rs @@ -76,11 +76,11 @@ pub fn fresh_report_count(env: &Env, symbol: &soroban_sdk::Symbol) -> u32 { let mut count: u32 = 0; for feeder in feeder_list(env).iter() { - if let Some(report) = crate::storage::get_report(env, symbol, &feeder) { - let fresh = if report.timestamp > now { + if let Some((_price, timestamp)) = crate::oracle_adapter::latest_report(env, symbol, &feeder) { + let fresh = if timestamp > now { true } else { - now.saturating_sub(report.timestamp) <= max_staleness + now.saturating_sub(timestamp) <= max_staleness }; if fresh { count += 1; diff --git a/price_oracle/src/test.rs b/price_oracle/src/test.rs index 5bb6071..82bfe87 100644 --- a/price_oracle/src/test.rs +++ b/price_oracle/src/test.rs @@ -8,7 +8,9 @@ use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger}, Address, BytesN, Env, Symbol, TryFromVal, }; -use zenith_common::testutils::advance_timestamp; +fn advance_timestamp(env: &Env, delta: u64) { + env.ledger().with_mut(|l| l.timestamp += delta); +} struct Harness<'a> { env: Env, @@ -624,6 +626,7 @@ fn set_min_reports_emits_a_min_reports_updated_event_with_the_new_value() { #[test] fn policy_getters_are_readable_without_authorization() { let env = Env::default(); + env.mock_all_auths(); let admin = Address::generate(&env); let contract_id = env.register_contract(None, PriceOracle); let client = PriceOracleClient::new(&env, &contract_id); diff --git a/staking/Cargo.toml b/staking/Cargo.toml index 6636cb4..9981c82 100644 --- a/staking/Cargo.toml +++ b/staking/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-staking" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] crate-type = ["cdylib", "rlib"] diff --git a/test_fixtures/Cargo.toml b/test_fixtures/Cargo.toml index 429a487..36ecd0c 100644 --- a/test_fixtures/Cargo.toml +++ b/test_fixtures/Cargo.toml @@ -12,6 +12,11 @@ soroban-sdk = { workspace = true, features = ["testutils"] } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } +multisig = { workspace = true, features = ["testutils"] } +price_oracle = { workspace = true, features = ["testutils"] } +vault = { workspace = true, features = ["testutils"] } +timelock = { workspace = true, features = ["testutils"] } +test_adversaries = { package = "zenith-test-adversaries", path = "../test_adversaries" } [features] # The fixtures are test-only by design; this feature gate makes the diff --git a/test_fixtures/src/multisig.rs b/test_fixtures/src/multisig.rs index 4119237..499d37f 100644 --- a/test_fixtures/src/multisig.rs +++ b/test_fixtures/src/multisig.rs @@ -17,7 +17,7 @@ //! assert!(ms.is_approved(action_id)); //! ``` -use soroban_sdk::{Address, Env, Map, Vec}; +use soroban_sdk::{testutils::Address as _, Address, Env, Map, Vec}; /// In-process multisig harness: tracks per-action approval counts. /// diff --git a/test_fixtures/src/oracle.rs b/test_fixtures/src/oracle.rs index 2c0ec7f..c05e869 100644 --- a/test_fixtures/src/oracle.rs +++ b/test_fixtures/src/oracle.rs @@ -18,7 +18,7 @@ //! assert!(oracle.get_price(&env, "XLM").is_none()); //! ``` -use soroban_sdk::{contracttype, Address, Env, Symbol, Vec}; +use soroban_sdk::{contracttype, testutils::Address as _, Address, Env, Symbol, Vec}; /// One feeder's price observation, stored in a Soroban-compatible struct. #[contracttype(export = false)] diff --git a/test_fixtures/src/token.rs b/test_fixtures/src/token.rs index 23ec767..1051d1b 100644 --- a/test_fixtures/src/token.rs +++ b/test_fixtures/src/token.rs @@ -11,7 +11,7 @@ //! assert_eq!(token.balance(&env, &user), 100_000_000); //! ``` -use soroban_sdk::{token, Address, Env}; +use soroban_sdk::{testutils::Address as _, token, Address, Env}; /// A deployed Stellar Asset Contract with a minting authority. pub struct TokenFixture { diff --git a/test_fixtures/tests/threat_model_negative.rs b/test_fixtures/tests/threat_model_negative.rs new file mode 100644 index 0000000..0fa5ba8 --- /dev/null +++ b/test_fixtures/tests/threat_model_negative.rs @@ -0,0 +1,762 @@ +#![cfg(test)] + +//! # THREAT_MODEL-Driven Negative Test Suite for Auth and Admin Bypass (Issue #198) +//! +//! This suite walks `THREAT_MODEL.md` line by line and asserts concrete, typed +//! rejections against live contract instances for identified authorization +//! and admin-bypass threat paths across the Zenith protocol contracts: +//! +//! - Section 1: Trust Boundaries & Privileged Actors (Admin, Multisig, Feeders, Timelock) +//! - Section 2.1: Rate-Limited Outflows & Circuit Breakers (Untested / Tracked follow-up #19) +//! - Section 2.2: Protocol Guardian & Auto-Expiring Pauses (Untested / Tracked follow-up #18) +//! - Section 2.3: Granular Access Control Roles (Untested / Tracked follow-up #17) +//! - Section 2.4: Flash-Loan / Oracle Price Manipulation & Feeder Consensus +//! - Section 2.5: Solvency Invariant & Cross-Tag Collateral Isolation in Vault +//! - Section 2.6: Multisig Quorum & Timelock Tiers (Tracked follow-up #150) + +extern crate std; + +use multisig::{Multisig, MultisigClient, SignerWeight}; +use price_oracle::{PriceOracle, PriceOracleClient}; +use soroban_sdk::{ + contract, contractimpl, + testutils::{Address as _, Ledger as _}, + vec, Address, BytesN, Env, IntoVal, Symbol, Vec, +}; +use test_adversaries::tokens::FeeOnTransferToken; +use timelock::{Call, Timelock, TimelockClient}; +use vault::{Tag, Vault, VaultClient}; + +fn contract_err(code: u32) -> soroban_sdk::Error { + soroban_sdk::Error::from_contract_error(code) +} + +// Stand-in target contract for timelock tests. +#[contract] +struct MockAdminTarget; + +#[contractimpl] +impl MockAdminTarget { + pub fn execute_action(env: Env, admin: Address, val: u32) { + admin.require_auth(); + env.storage().instance().set(&0u32, &val); + } + + pub fn get_val(env: Env) -> u32 { + env.storage().instance().get(&0u32).unwrap_or(0) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Section 1 & Core Requirement 1: Non-Admin Admin Entrypoint Bypass Rejections +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn threat_1_vault_unauthorized_non_admin_cannot_transfer_admin() { + let env = Env::default(); + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin).address(); + let vault_id = env.register_contract(None, Vault); + let client = VaultClient::new(&env, &vault_id); + + // Initialize with admin auth. + env.mock_all_auths(); + client.initialize(&admin, &token); + + // Clear mock auths: non-admin call must fail authorization. + env.mock_auths(&[]); + let non_admin = Address::generate(&env); + let res = client.try_transfer_admin(&non_admin); + assert!(res.is_err(), "transfer_admin must reject non-admin call"); +} + +#[test] +fn threat_1_vault_unapproved_multisig_admin_actions_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin).address(); + let vault_id = env.register_contract(None, Vault); + let client = VaultClient::new(&env, &vault_id); + client.initialize(&admin, &token); + + let ms_id = env.register_contract(None, Multisig); + let ms_client = MultisigClient::new(&env, &ms_id); + let signers = vec![ + &env, + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ]; + ms_client.initialize(&signers, &2, &0); + + let unapproved_action = BytesN::from_array(&env, &[99u8; 32]); + let new_admin = Address::generate(&env); + + // transfer_admin_via_multisig with unapproved action fails Unauthorized (error #6) + let res = client.try_transfer_admin_via_multisig(&ms_id, &unapproved_action, &new_admin); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::Unauthorized as u32))), + "transfer_admin_via_multisig must reject unapproved action with Unauthorized" + ); + + // pause_via_multisig with unapproved action fails Unauthorized + let res = client.try_pause_via_multisig(&ms_id, &unapproved_action); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::Unauthorized as u32))), + "pause_via_multisig must reject unapproved action with Unauthorized" + ); + + // unpause_via_multisig with unapproved action fails Unauthorized + let res = client.try_unpause_via_multisig(&ms_id, &unapproved_action); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::Unauthorized as u32))), + "unpause_via_multisig must reject unapproved action with Unauthorized" + ); + + // sweep_untagged_via_multisig with unapproved action fails Unauthorized + let res = client.try_sweep_untagged_via_multisig(&ms_id, &unapproved_action, &token, &new_admin); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::Unauthorized as u32))), + "sweep_untagged_via_multisig must reject unapproved action with Unauthorized" + ); +} + +#[test] +fn threat_1_price_oracle_unauthorized_non_admin_cannot_mutate_config() { + let env = Env::default(); + let admin = Address::generate(&env); + let oracle_id = env.register_contract(None, PriceOracle); + let client = PriceOracleClient::new(&env, &oracle_id); + + env.mock_all_auths(); + client.initialize(&admin); + + let stranger = Address::generate(&env); + let ms_id = env.register_contract(None, Multisig); + let unapproved_action = BytesN::from_array(&env, &[99u8; 32]); + + // Via multisig with unapproved action must reject with Unauthorized (error #9). + let res = client.try_add_feeder_via_multisig(&ms_id, &unapproved_action, &stranger); + assert_eq!( + res.err(), + Some(Ok(contract_err(price_oracle::Error::Unauthorized as u32))), + "add_feeder_via_multisig must reject unapproved action" + ); + + let res = client.try_set_min_reports_via_multisig(&ms_id, &unapproved_action, &3); + assert_eq!( + res.err(), + Some(Ok(contract_err(price_oracle::Error::Unauthorized as u32))), + "set_min_reports_via_multisig must reject unapproved action" + ); + + let res = client.try_set_max_staleness_via_multisig(&ms_id, &unapproved_action, &7200); + assert_eq!( + res.err(), + Some(Ok(contract_err(price_oracle::Error::Unauthorized as u32))), + "set_max_staleness_via_multisig must reject unapproved action" + ); + + let res = client.try_transfer_admin_via_multisig(&ms_id, &unapproved_action, &stranger); + assert_eq!( + res.err(), + Some(Ok(contract_err(price_oracle::Error::Unauthorized as u32))), + "transfer_admin_via_multisig must reject unapproved action" + ); +} + +#[test] +fn threat_1_timelock_unauthorized_roles_cannot_schedule_execute_or_cancel() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let proposer = Address::generate(&env); + let executor = Address::generate(&env); + let canceller = Address::generate(&env); + let stranger = Address::generate(&env); + + let tl_id = env.register_contract(None, Timelock); + let tl_client = TimelockClient::new(&env, &tl_id); + + let proposers = vec![&env, proposer.clone()]; + let executors = vec![&env, executor.clone()]; + let cancellers = vec![&env, canceller.clone()]; + tl_client.initialize(&86_400, &proposers, &executors, &cancellers); + + let target_id = env.register_contract(None, MockAdminTarget); + let calls = vec![ + &env, + Call { + target: target_id, + function: Symbol::new(&env, "execute_action"), + args: vec![&env, admin.to_val(), 42u32.into_val(&env)], + }, + ]; + let salt = BytesN::from_array(&env, &[1u8; 32]); + + // Stranger attempting to schedule -> Unauthorized (Error #2) + let res = tl_client.try_schedule(&stranger, &calls, &None, &salt, &86_400); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::Unauthorized as u32))), + "Non-proposer scheduling must fail with Unauthorized" + ); + + // Valid schedule by proposer + let op_id = tl_client.schedule(&proposer, &calls, &None, &salt, &86_400); + let ready_at = env.ledger().timestamp() + 86_400; + + // Stranger attempting to execute -> Unauthorized (Error #2) + env.ledger().set_timestamp(ready_at); + let res = tl_client.try_execute(&stranger, &calls, &None, &salt); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::Unauthorized as u32))), + "Non-executor executing must fail with Unauthorized" + ); + + // Stranger attempting to cancel -> Unauthorized (Error #2) + let res = tl_client.try_cancel(&stranger, &op_id); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::Unauthorized as u32))), + "Non-canceller cancelling must fail with Unauthorized" + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Section 2.4 & Core Requirement 2: Price Oracle Consensus & Feeder Quorum +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn threat_2_single_feeder_cannot_determine_aggregate_alone() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle_id = env.register_contract(None, PriceOracle); + let client = PriceOracleClient::new(&env, &oracle_id); + client.initialize(&admin); + + let f1 = Address::generate(&env); + let f2 = Address::generate(&env); + client.add_feeder(&f1); + client.add_feeder(&f2); + + // Require 2 feeders for quorum. + client.set_min_reports(&2); + + let xlm = Symbol::new(&env, "XLM"); + + // Single feeder reports. + client.report_price(&f1, &xlm, &1_200_000); + + // Aggregate MUST fail closed: None returned because quorum (2) is not met. + assert_eq!( + client.get_price(&xlm), + None, + "Single feeder must not determine aggregate when min_reports = 2" + ); + + // When second feeder reports, aggregate is determined. + client.report_price(&f2, &xlm, &1_400_000); + assert_eq!( + client.get_price(&xlm), + Some(1_300_000), + "Two feeders satisfy quorum and produce median aggregate" + ); +} + +#[test] +fn threat_2_unauthorized_feeder_cannot_report_price() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle_id = env.register_contract(None, PriceOracle); + let client = PriceOracleClient::new(&env, &oracle_id); + client.initialize(&admin); + + let stranger = Address::generate(&env); + let xlm = Symbol::new(&env, "XLM"); + + // An unregistered address calling report_price -> NotAFeeder (Error #4) + let res = client.try_report_price(&stranger, &xlm, &1_500_000); + assert_eq!( + res.err(), + Some(Ok(contract_err(price_oracle::Error::NotAFeeder as u32))), + "Unauthorized feeder reporting must reject with NotAFeeder" + ); +} + +#[test] +fn threat_2_minority_outlier_feeder_cannot_skew_median_beyond_honest_bounds() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle_id = env.register_contract(None, PriceOracle); + let client = PriceOracleClient::new(&env, &oracle_id); + client.initialize(&admin); + + let f1 = Address::generate(&env); + let f2 = Address::generate(&env); + let f3_adversary = Address::generate(&env); + client.add_feeder(&f1); + client.add_feeder(&f2); + client.add_feeder(&f3_adversary); + client.set_min_reports(&2); + + let xlm = Symbol::new(&env, "XLM"); + + // Honest feeders report normal prices + client.report_price(&f1, &xlm, &1_000_000); + client.report_price(&f2, &xlm, &1_020_000); + + // Adversarial feeder tries extreme price manipulation (100x skew) + client.report_price(&f3_adversary, &xlm, &100_000_000); + + // Median of [1_000_000, 1_020_000, 100_000_000] is 1_020_000 + // The aggregate is bounded by honest observations; the outlier cannot move it alone. + let aggregate = client.get_price(&xlm).unwrap(); + assert_eq!(aggregate, 1_020_000); + assert!( + aggregate >= 1_000_000 && aggregate <= 1_020_000, + "Outlier feeder must not dictate the median aggregate" + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Section 2.5 & Core Requirement 3: Vault Solvency & Cross-Tag Collateral Isolation +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn threat_3_cross_tag_withdrawal_cannot_drain_another_tags_deposit() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token = token_contract.address(); + let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); + + let vault_id = env.register_contract(None, Vault); + let client = VaultClient::new(&env, &vault_id); + client.initialize(&admin, &token); + + let integrator = Address::generate(&env); + client.set_integrator(&integrator, &true); + + let tag_a = Tag { + owner: integrator.clone(), + kind: Symbol::new(&env, "series"), + id: 1, + }; + let tag_b = Tag { + owner: integrator.clone(), + kind: Symbol::new(&env, "series"), + id: 2, + }; + + let user_a = Address::generate(&env); + let payee_b = Address::generate(&env); + + // User A funds Tag A with 1,000,000 tokens + sac.mint(&user_a, &1_000_000); + client.deposit(&user_a, &token, &tag_a, &1_000_000); + + // Tag B has 0 escrowed. Vault holds 1,000,000 raw token balance from Tag A. + assert_eq!(client.balance_of(&token, &tag_a), 1_000_000); + assert_eq!(client.balance_of(&token, &tag_b), 0); + + // Attempting to withdraw from Tag B must be rejected with InsufficientEscrowBalance (Error #3) + let res = client.try_withdraw(&token, &tag_b, &payee_b, &500_000); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::InsufficientEscrowBalance as u32))), + "Withdrawal from tag B must not touch collateral deposited under tag A" + ); + + // Verify tag A's collateral is intact + assert_eq!(client.balance_of(&token, &tag_a), 1_000_000); +} + +#[test] +fn threat_3_unregistered_integrator_cannot_create_tags() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin).address(); + + let vault_id = env.register_contract(None, Vault); + let client = VaultClient::new(&env, &vault_id); + client.initialize(&admin, &token); + + let rogue = Address::generate(&env); + let rogue_tag = Tag { + owner: rogue.clone(), + kind: Symbol::new(&env, "series"), + id: 1, + }; + + // Rogue is not registered via set_integrator -> UnregisteredIntegrator (Error #8) + let res = client.try_deposit(&rogue, &token, &rogue_tag, &500_000); + assert_eq!( + res.err(), + Some(Ok(contract_err(vault::Error::UnregisteredIntegrator as u32))), + "Unregistered integrator cannot create tags" + ); +} + +#[test] +fn threat_3_vault_delta_check_protects_against_fee_on_transfer_undercollateralization() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_id = env.register_contract(None, FeeOnTransferToken); + let depositor = Address::generate(&env); + + // Initialize FeeOnTransferToken (10% fee on transfer) + env.invoke_contract::<()>( + &token_id, + &Symbol::new(&env, "initialize"), + vec![&env, depositor.to_val()], + ); + env.invoke_contract::<()>( + &token_id, + &Symbol::new(&env, "mint"), + vec![&env, depositor.to_val(), 1_000_000i128.into_val(&env)], + ); + + let vault_id = env.register_contract(None, Vault); + let client = VaultClient::new(&env, &vault_id); + client.initialize(&admin, &token_id); + client.set_integrator(&depositor, &true); + + let tag = Tag { + owner: depositor.clone(), + kind: Symbol::new(&env, "series"), + id: 42, + }; + + // Depositor deposits 1,000,000 tokens. + // 10% fee means only 900,000 arrives in vault. + // Vault measures before/after delta and credits ONLY 900,000. + let credited = client.deposit(&depositor, &token_id, &tag, &1_000_000); + assert_eq!( + credited, 900_000, + "Vault must credit only actual received delta" + ); + assert_eq!(client.balance_of(&token_id, &tag), 900_000); + assert_eq!(client.get_total_escrowed(&token_id), 900_000); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Section 2.6 & Core Requirement 4: Multisig Signer Bypass & Quorum Enforcement +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn threat_4_signer_approving_outside_allowed_set_is_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let signers = vec![ + &env, + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ]; + let ms_id = env.register_contract(None, Multisig); + let client = MultisigClient::new(&env, &ms_id); + client.initialize(&signers, &2, &0); + + let stranger = Address::generate(&env); + let action_id = BytesN::from_array(&env, &[77u8; 32]); + + // Stranger attempting approval -> NotASigner (Error #4) + let res = client.try_approve(&stranger, &action_id); + assert_eq!( + res.err(), + Some(Ok(multisig::Error::NotASigner)), + "Non-signer approval must reject with NotASigner" + ); + + // Stranger attempting to propose signer change -> NotASigner (Error #4) + let new_signer = Address::generate(&env); + let res = client.try_propose_signer_change( + &stranger, + &vec![&env, SignerWeight { address: new_signer, weight: 1u32 }], + &Vec::new(&env), + &2, + &3, + ); + assert_eq!( + res.err(), + Some(Ok(multisig::Error::NotASigner)), + "Non-signer proposing signer change must reject with NotASigner" + ); +} + +#[test] +fn threat_4_multisig_action_execution_below_threshold_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let signers = vec![ + &env, + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ]; + let ms_id = env.register_contract(None, Multisig); + let client = MultisigClient::new(&env, &ms_id); + client.initialize(&signers, &2, &0); // 2-of-3 required + + let target = env.register_contract(None, MockAdminTarget); + let id = client.propose( + &signers.get(0).unwrap(), + &target, + &Symbol::new(&env, "execute_action"), + &vec![&env, ms_id.to_val(), 10u32.into_val(&env)], + ); + + // Only 1 signer approves (below threshold of 2) + client.approve(&signers.get(0).unwrap(), &id); + assert!(!client.is_approved(&id)); + + // Execution attempt below threshold -> NotYetApproved (Error #6) + let res = client.try_execute(&id); + assert_eq!( + res.err(), + Some(Ok(multisig::Error::NotYetApproved)), + "Execution below threshold must reject with NotYetApproved" + ); +} + +#[test] +fn threat_4_multisig_duplicate_approval_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let signers = vec![ + &env, + Address::generate(&env), + Address::generate(&env), + ]; + let ms_id = env.register_contract(None, Multisig); + let client = MultisigClient::new(&env, &ms_id); + client.initialize(&signers, &2, &0); + + let action_id = BytesN::from_array(&env, &[12u8; 32]); + client.approve(&signers.get(0).unwrap(), &action_id); + + // Replay approval from same signer -> AlreadyApproved (Error #5) + let res = client.try_approve(&signers.get(0).unwrap(), &action_id); + assert_eq!( + res.err(), + Some(Ok(multisig::Error::AlreadyApproved)), + "Duplicate approval from same signer must reject with AlreadyApproved" + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Section 1, 2.4 & Core Requirement 5: Timelock Early Execution & Delay Bypass +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn threat_5_timelock_action_executed_before_delay_fails_not_ready() { + let env = Env::default(); + env.mock_all_auths(); + + let proposer = Address::generate(&env); + let executor = Address::generate(&env); + let canceller = Address::generate(&env); + + let tl_id = env.register_contract(None, Timelock); + let client = TimelockClient::new(&env, &tl_id); + + let delay = 86_400u64; + client.initialize( + &delay, + &vec![&env, proposer.clone()], + &vec![&env, executor.clone()], + &vec![&env, canceller.clone()], + ); + + let target = env.register_contract(None, MockAdminTarget); + let calls = vec![ + &env, + Call { + target, + function: Symbol::new(&env, "execute_action"), + args: vec![&env, tl_id.to_val(), 100u32.into_val(&env)], + }, + ]; + let salt = BytesN::from_array(&env, &[55u8; 32]); + + let op_id = client.schedule(&proposer, &calls, &None, &salt, &delay); + + // Advance time by (delay - 1 second) + env.ledger().set_timestamp(env.ledger().timestamp() + delay - 1); + assert!(!client.is_operation_ready(&op_id)); + + // Execute before delay has elapsed -> NotReady (Error #5) + let res = client.try_execute(&executor, &calls, &None, &salt); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::NotReady as u32))), + "Executing timelock action before delay has elapsed must fail with NotReady" + ); + + // Advance 1 more second to exact ready_at boundary -> execution succeeds + env.ledger().set_timestamp(env.ledger().timestamp() + 1); + assert!(client.is_operation_ready(&op_id)); + client.execute(&executor, &calls, &None, &salt); + assert!(client.is_operation_done(&op_id)); +} + +#[test] +fn threat_5_timelock_delay_below_minimum_fails_insufficient_delay() { + let env = Env::default(); + env.mock_all_auths(); + + let proposer = Address::generate(&env); + let executor = Address::generate(&env); + let canceller = Address::generate(&env); + + let tl_id = env.register_contract(None, Timelock); + let client = TimelockClient::new(&env, &tl_id); + + let min_delay = 86_400u64; + client.initialize( + &min_delay, + &vec![&env, proposer.clone()], + &vec![&env, executor.clone()], + &vec![&env, canceller.clone()], + ); + + let target = env.register_contract(None, MockAdminTarget); + let calls = vec![ + &env, + Call { + target, + function: Symbol::new(&env, "execute_action"), + args: vec![&env, tl_id.to_val(), 1u32.into_val(&env)], + }, + ]; + let salt = BytesN::from_array(&env, &[22u8; 32]); + + // Requesting delay < min_delay -> InsufficientDelay (Error #3) + let res = client.try_schedule(&proposer, &calls, &None, &salt, &(min_delay - 1)); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::InsufficientDelay as u32))), + "Scheduling with delay below min_delay must reject with InsufficientDelay" + ); +} + +#[test] +fn threat_5_timelock_dependent_cannot_execute_before_predecessor() { + let env = Env::default(); + env.mock_all_auths(); + + let proposer = Address::generate(&env); + let executor = Address::generate(&env); + let canceller = Address::generate(&env); + + let tl_id = env.register_contract(None, Timelock); + let client = TimelockClient::new(&env, &tl_id); + let delay = 3600u64; + client.initialize( + &delay, + &vec![&env, proposer.clone()], + &vec![&env, executor.clone()], + &vec![&env, canceller.clone()], + ); + + let target = env.register_contract(None, MockAdminTarget); + let calls_1 = vec![ + &env, + Call { + target: target.clone(), + function: Symbol::new(&env, "execute_action"), + args: vec![&env, tl_id.to_val(), 1u32.into_val(&env)], + }, + ]; + let calls_2 = vec![ + &env, + Call { + target, + function: Symbol::new(&env, "execute_action"), + args: vec![&env, tl_id.to_val(), 2u32.into_val(&env)], + }, + ]; + let salt_1 = BytesN::from_array(&env, &[10u8; 32]); + let salt_2 = BytesN::from_array(&env, &[20u8; 32]); + + let pred_id = client.schedule(&proposer, &calls_1, &None, &salt_1, &delay); + let ready_at_2 = env.ledger().timestamp() + delay; + let _ = client.schedule(&proposer, &calls_2, &Some(pred_id.clone()), &salt_2, &delay); + + // Advance time past delay for both operations + env.ledger().set_timestamp(ready_at_2); + + // Attempting to execute dependent operation before predecessor -> PredecessorNotDone (Error #6) + let res = client.try_execute(&executor, &calls_2, &Some(pred_id), &salt_2); + assert_eq!( + res.err(), + Some(Ok(contract_err(timelock::Error::PredecessorNotDone as u32))), + "Dependent operation cannot execute before predecessor is done" + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// THREAT_MODEL Gaps & Unimplemented Features (Tracked Follow-ups) +// ───────────────────────────────────────────────────────────────────────────── + +/// THREAT_MODEL Section 2.1: Per-window cumulative outflow limits and automated +/// withdrawal circuit breakers on vault and options_market. +#[test] +#[ignore = "issue #19: rate-limited outflows and withdrawal circuit breakers not yet implemented"] +fn threat_unverified_section_2_1_rate_limited_outflow_circuit_breaker() { + // Tracked follow-up: assert per-window cumulative outflow cap rejects + // rapid liquidation drains once circuit breaker feature lands (issue #19). +} + +/// THREAT_MODEL Section 2.2: Protocol Guardian auto-expiring pause mechanism. +#[test] +#[ignore = "issue #18: auto-expiring pauses and protocol guardian role not yet implemented"] +fn threat_unverified_section_2_2_protocol_guardian_auto_expiring_pauses() { + // Tracked follow-up: assert pause automatically expires after MAX_PAUSE_DURATION + // once Protocol Guardian role lands (issue #18). +} + +/// THREAT_MODEL Section 2.3: Granular access control roles (GUARDIAN, ORACLE_ADMIN, +/// FEE_MANAGER, EMERGENCY_SETTLER in separate zenith-access crate). +#[test] +#[ignore = "issue #17: granular access control roles not yet implemented"] +fn threat_unverified_section_2_3_granular_access_control_roles() { + // Tracked follow-up: assert separation of privileged roles once zenith-access + // crate lands (issue #17). +} + +/// THREAT_MODEL Section 2.6: Multisig timelock tiers (`is_executable` on multisig contract). +#[test] +#[ignore = "issue #150: multisig is_executable per-ActionClass delay enforcement not yet implemented"] +fn threat_unverified_section_2_6_multisig_timelock_tiers_is_executable() { + // Tracked follow-up: assert per-class delays (Emergency=0, Standard=24h, Critical=72h) + // are enforced directly on multisig contract entrypoints once issue #150 lands. +} diff --git a/timelock/src/lib.rs b/timelock/src/lib.rs index 4968d5b..f188cf3 100644 --- a/timelock/src/lib.rs +++ b/timelock/src/lib.rs @@ -66,14 +66,14 @@ use soroban_sdk::{ #[cfg(test)] mod test; -mod error; +pub mod error; mod events; mod migrations; -mod types; +pub mod types; -use error::Error; -use types::{is_no_predecessor, Call, DataKey, Operation, Role}; -pub use types::{Call as TimelockCall, Role as TimelockRole}; +pub use error::Error; +use types::{is_no_predecessor, DataKey, Operation}; +pub use types::{Call, Call as TimelockCall, Role, Role as TimelockRole}; #[contract] pub struct Timelock; @@ -176,7 +176,9 @@ fn self_call(env: &Env, func: &Symbol, args: &Vec) { env.storage().instance().set(&DataKey::OpenExecutor, &open); events::open_executor_changed(env, open); } else if *func == Symbol::new(env, "migrate") { - migrations::self_migrate(env, arg(env, args, 0), arg(env, args, 1)); + if let Err(e) = migrations::self_migrate(env, arg(env, args, 0), arg(env, args, 1)) { + panic_with_error!(env, e); + } } else { panic_with_error!(env, Error::UnknownSelfCall); } diff --git a/timelock/src/migrations.rs b/timelock/src/migrations.rs index 5079f21..b59e8ab 100644 --- a/timelock/src/migrations.rs +++ b/timelock/src/migrations.rs @@ -53,22 +53,22 @@ pub const ERRORS: MigrationErrors = MigrationErrors { /// /// A step over unbounded data processes items `[cursor, cursor + limit)` /// and returns `Step::More(cursor + processed)` until it's done. -pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Step { - let _ = (cursor, limit); +pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Result { + let _ = (env, cursor, limit); #[allow(clippy::match_single_binding)] match to { - _ => panic_with_error!(env, Error::InvalidMigrationTarget), + _ => Err(Error::InvalidMigrationTarget), } } /// The `MigrationInProgress` guard for state-changing entrypoints. pub fn require_current(env: &Env) { - migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress); + let _ = migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress); } /// Target of a timelock self-call `migrate(from, to)`; only reachable /// through a ready operation in `execute`. -pub fn self_migrate(env: &Env, from: u32, to: u32) -> u32 { +pub fn self_migrate(env: &Env, from: u32, to: u32) -> Result { migrations::migrate(env, from, to, CURRENT_SCHEMA_VERSION, step, &ERRORS) } @@ -77,7 +77,7 @@ impl Timelock { /// Continues a paginated migration from `cursor`. Permissionless, like /// `bump`: the target was fixed by the delayed `migrate` self-call and /// the steps are code in this wasm, so a keeper can only advance it. - pub fn migrate_batch(env: Env, cursor: u32, limit: u32) -> u32 { + pub fn migrate_batch(env: Env, cursor: u32, limit: u32) -> Result { migrations::migrate_batch(&env, cursor, limit, step, &ERRORS) } diff --git a/vault/Cargo.toml b/vault/Cargo.toml index 98cefa3..2919581 100644 --- a/vault/Cargo.toml +++ b/vault/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-vault" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so other contracts (options_market) can depend on diff --git a/vault/src/lib.rs b/vault/src/lib.rs index b3c4b5d..2375c1f 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -26,13 +26,13 @@ use soroban_sdk::{ #[cfg(test)] mod test; -mod error; +pub mod error; mod events; mod migrations; mod ttl; -mod types; +pub mod types; -use error::Error; +pub use error::Error; pub use types::Tag; use types::{DataKey, LegacyKey}; diff --git a/vault/src/migrations.rs b/vault/src/migrations.rs index 1c56e06..649c754 100644 --- a/vault/src/migrations.rs +++ b/vault/src/migrations.rs @@ -21,7 +21,7 @@ use soroban_sdk::{contractimpl, panic_with_error, Address, BytesN, Env}; use zenith_common::migrations::{self, MigrationErrors, Step}; -use zenith_common::upgrade::{self, UpgradeAuth}; +use zenith_common::upgrade; use zenith_common::{require_admin_or_multisig, ActionClass, Auth}; use crate::error::Error; @@ -49,11 +49,11 @@ pub const ERRORS: MigrationErrors = MigrationErrors { /// /// A step over unbounded data processes items `[cursor, cursor + limit)` /// and returns `Step::More(cursor + processed)` until it's done. -pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Step { +pub fn step(env: &Env, to: u32, cursor: u32, limit: u32) -> Result { let _ = (cursor, limit); match to { - 2 => v2(env), - _ => panic_with_error!(env, Error::InvalidMigrationTarget), + 2 => Ok(v2(env)), + _ => Err(Error::InvalidMigrationTarget), } } @@ -84,11 +84,14 @@ pub(crate) fn preserving_ledger(env: &Env, f: impl FnOnce() -> u32) -> u32 { /// The `MigrationInProgress` guard for state-changing entrypoints. pub fn require_current(env: &Env) { - migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress); + let _ = migrations::require_schema_current(env, CURRENT_SCHEMA_VERSION, Error::MigrationInProgress) + .map_err(|e| panic_with_error!(env, e)); } fn require_admin(env: &Env) { - zenith_common::get_admin(env, &DataKey::Admin).require_auth(); + zenith_common::get_admin(env, &DataKey::Admin, Error::Unauthorized) + .unwrap_or_else(|e| panic_with_error!(env, e)) + .require_auth(); } /// Upgrade and migration entrypoints. A separate `#[contractimpl]` block @@ -101,13 +104,8 @@ impl Vault { /// fail with `MigrationInProgress`. pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { ttl::extend_instance(&env); - upgrade::upgrade( - &env, - &DataKey::Admin, - UpgradeAuth::Admin, - new_wasm_hash, - Error::Unauthorized, - ); + require_admin(&env); + env.deployer().update_current_contract_wasm(new_wasm_hash); } /// Permissionless alternative to `upgrade`, authorized by the @@ -115,15 +113,16 @@ impl Vault { /// executable as `Critical`. `action_id` must equal /// `upgrade_action_id(new_wasm_hash)`, so the approval covers exactly /// this wasm on exactly this contract. - pub fn upgrade_via_multisig(env: Env, action_id: BytesN<32>, new_wasm_hash: BytesN<32>) { + pub fn upgrade_via_multisig(env: Env, action_id: u64, new_wasm_hash: BytesN<32>) { ttl::extend_instance(&env); - upgrade::upgrade( - &env, - &DataKey::Admin, - UpgradeAuth::PinnedMultisig(action_id), - new_wasm_hash, - Error::Unauthorized, - ); + let multisig = upgrade::pinned_multisig(&env) + .unwrap_or_else(|| panic_with_error!(&env, Error::Unauthorized)); + if action_id != upgrade::upgrade_action_id(&env, &new_wasm_hash) + || !zenith_common::is_executable(&env, &multisig, action_id, ActionClass::Critical) + { + panic_with_error!(&env, Error::Unauthorized); + } + env.deployer().update_current_contract_wasm(new_wasm_hash); } /// Pins the multisig `upgrade_via_multisig` and `migrate_via_multisig` @@ -141,7 +140,7 @@ impl Vault { /// The multisig action id that authorizes upgrading this contract to /// `new_wasm_hash` — what signers approve. - pub fn upgrade_action_id(env: Env, new_wasm_hash: BytesN<32>) -> BytesN<32> { + pub fn upgrade_action_id(env: Env, new_wasm_hash: BytesN<32>) -> u64 { upgrade::upgrade_action_id(&env, &new_wasm_hash) } @@ -152,6 +151,7 @@ impl Vault { require_admin(&env); preserving_ledger(&env, || { migrations::migrate(&env, from, to, CURRENT_SCHEMA_VERSION, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) }) } @@ -159,19 +159,21 @@ impl Vault { /// multisig. `Emergency` class (no extra delay): the steps it runs /// were already timelocked as `Critical` with the upgrade that /// installed them, and the contract is blocked until they run. - pub fn migrate_via_multisig(env: Env, action_id: BytesN<32>, from: u32, to: u32) -> u32 { + pub fn migrate_via_multisig(env: Env, action_id: u64, from: u32, to: u32) -> u32 { ttl::extend_instance(&env); let multisig = upgrade::pinned_multisig(&env) .unwrap_or_else(|| panic_with_error!(&env, Error::Unauthorized)); - require_admin_or_multisig( + let _ = require_admin_or_multisig( &env, &DataKey::Admin, &Auth::Multisig(multisig, action_id), ActionClass::Emergency, Error::Unauthorized, + Error::Unauthorized, ); preserving_ledger(&env, || { migrations::migrate(&env, from, to, CURRENT_SCHEMA_VERSION, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) }) } @@ -182,6 +184,7 @@ impl Vault { ttl::extend_instance(&env); preserving_ledger(&env, || { migrations::migrate_batch(&env, cursor, limit, step, &ERRORS) + .unwrap_or_else(|e| panic_with_error!(&env, e)) }) }