From 691f6cc253837283398dfca58190dbbf619ae8d8 Mon Sep 17 00:00:00 2001 From: beebozy Date: Tue, 28 Apr 2026 16:55:16 -0700 Subject: [PATCH 1/2] help modify the vesting contract --- Cargo.lock | 1612 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +- rust-toolchain.toml | 2 +- src/lib.rs | 153 +--- src/vesting.rs | 16 + 5 files changed, 1637 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e69de29bb..f1f160fc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -0,0 +1,1612 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[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", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "revora-contracts" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" +dependencies = [ + "backtrace", + "curve25519-dalek", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6edf92749fd8399b417192d301c11f710b9cdce15789a3d157785ea971576fa" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcdf04484af7cc731a7a48ad1d9f5f940370edeea84734434ceaf398a6b862e" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "derive_arbitrary", + "ed25519-dalek", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" +dependencies = [ + "crate-git-revision", + "darling 0.20.11", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-spec" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dafbde981b141b191c6c036abc86097070ddd6eaaa33b273701449501e43d3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index e6fa420c7..e5c2738df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = { version = "22.0.0", features = ["alloc"] } +soroban-sdk = { version = "21.0.0", features = ["alloc"] } [dev-dependencies] -derive_arbitrary = "=1.3.2" -proptest = "1.6.0" -soroban-sdk = { version = "20.5.0", features = ["testutils", "alloc"] } -proptest = "1.4.0" + +soroban-sdk = { version = "21.0.0", features = ["testutils", "alloc"] } + + [profile.release] opt-level = "z" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6372b5f22..77f06cdb9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.79.0" +channel = "stable" components = ["clippy", "rustfmt"] targets = ["wasm32-unknown-unknown"] diff --git a/src/lib.rs b/src/lib.rs index 4e93f6236..9d7515e86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5696,155 +5696,6 @@ impl RevoraRevenueShare { } // ─── Revenue Deposit Contract (secondary contract) ─────────────────────────── -pub mod revenue_deposit { -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, Address, Env, Map, Symbol, Vec, - token::Client as TokenClient, -}; -use crate::{ - DataKey as RevoraDataKey, RevoraError, EventIndexTopicV2, - EVENT_TYPE_OFFER, EVENT_TYPE_REV_INIT, EVENT_TYPE_REV_OVR, EVENT_TYPE_REV_REJ, - EVENT_TYPE_REV_REP, EVENT_TYPE_CLAIM, BPS_DENOMINATOR, CONTRACT_VERSION, -}; - - env.events().publish((EVENT_PROPOSAL_EXECUTED, executor), proposal_id); - Ok(()) - } - - pub fn calculate_fee_for_asset( - _env: Env, - _issuer: Address, - _namespace: Symbol, - _token: Address, - _asset: Address, - _amount: i128, - ) -> i128 { - 0i128 - } - - let stored_version = - env.storage().persistent().get(&DataKey::DeployedVersion).unwrap_or(0u32); - - if stored_version == CONTRACT_VERSION { - return Err(RevoraError::AlreadyAtTargetVersion); - } - - if stored_version > CONTRACT_VERSION { - return Err(RevoraError::MigrationDowngradeNotAllowed); - } - - // Run migration hooks sequentially - for version in (stored_version + 1)..=CONTRACT_VERSION { - Self::run_migration_hook(&env, version)?; - } - - env.storage().persistent().set(&DataKey::DeployedVersion, &CONTRACT_VERSION); - - env.events() - .publish((symbol_short!("migrated"), admin), (stored_version, CONTRACT_VERSION)); - - Ok(CONTRACT_VERSION) - } - - /// Internal helper to run migration logic for a specific version bump. - fn run_migration_hook(env: &Env, version: u32) -> Result<(), RevoraError> { - match version { - 1 => { - // Initial version setup if needed (usually handled by initialize) - } - 2 => { - // Example v2 migration logic - } - 3 => { - // Example v3 migration logic - } - 4 => { - // Example v4 migration logic - } - _ => { - // Future versions will be handled here - } - } - Ok(()) - } - - /// Return the current deployed version of the contract state. - pub fn get_deployed_version(env: Env) -> u32 { - env.storage().persistent().get(&DataKey::DeployedVersion).unwrap_or(0) - } - - /// Return the current contract version (#23). Used for upgrade compatibility and migration. - pub fn get_version(env: Env) -> u32 { - let _ = env; - CONTRACT_VERSION - } - - /// Deterministic fixture payloads for indexer integration tests (#187). - /// - /// Returns canonical v2 indexed topics in a stable order so indexers can - /// validate decoding, routing and storage schemas without replaying full - /// contract flows. - pub fn get_indexer_fixture_topics( - env: Env, - issuer: Address, - namespace: Symbol, - token: Address, - period_id: u64, - ) -> Vec { - let mut fixtures = Vec::new(&env); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_OFFER, - issuer: issuer.clone(), - namespace: namespace.clone(), - token: token.clone(), - period_id: 0, - }); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_REV_INIT, - issuer: issuer.clone(), - namespace: namespace.clone(), - token: token.clone(), - period_id, - }); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_REV_OVR, - issuer: issuer.clone(), - namespace: namespace.clone(), - token: token.clone(), - period_id, - }); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_REV_REJ, - issuer: issuer.clone(), - namespace: namespace.clone(), - token: token.clone(), - period_id, - }); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_REV_REP, - issuer: issuer.clone(), - namespace: namespace.clone(), - token: token.clone(), - period_id, - }); - fixtures.push_back(EventIndexTopicV2 { - version: 2, - event_type: EVENT_TYPE_CLAIM, - issuer, - namespace, - token, - period_id: 0, - }); - fixtures - } - } -} - /// Deterministic fixture payloads for indexer integration tests (#187). /// /// Returns canonical v2 indexed topics in a stable order so indexers can @@ -5988,8 +5839,10 @@ use crate::{ }); fixtures } -} +// ── Test modules ───────────────────────────────────────────────────────────── + +// close impl RevoraRevenueShare // ── Test modules ───────────────────────────────────────────────────────────── // Each file uses `use crate::...` and `#![cfg(test)]` so they are only compiled diff --git a/src/vesting.rs b/src/vesting.rs index 185328d83..99673f7e3 100644 --- a/src/vesting.rs +++ b/src/vesting.rs @@ -7,6 +7,7 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, Env, Symbol, }; +/// Errors produced by the vesting module. #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] @@ -22,6 +23,21 @@ pub enum VestingError { AmendmentNotAllowed = 9, } + +/// A single vesting tranche for a beneficiary. +/// +/// # Fields +/// * `beneficiary` – Recipient of vested tokens. +/// * `token` – SEP-41 token contract address. +/// * `total_amount` – Total tokens to vest (must be > 0). +/// * `cliff_time` – Unix timestamp before which *nothing* unlocks. +/// * `start_time` – Vesting start for linear portion (must be ≥ `cliff_time`). +/// * `end_time` – Full-vest timestamp (must be > `start_time`). +/// +/// Tokens vest linearly from `start_time` to `end_time`. Between `cliff_time` +/// and `start_time` the vested amount is 0 (pure cliff). After `end_time` +/// the full `total_amount` is vested. + #[contracttype] #[derive(Clone, Debug, PartialEq)] pub struct VestingSchedule { From 7ecd78c62b385fd1e722d445720702c49ec5d6ad Mon Sep 17 00:00:00 2001 From: beebozy Date: Tue, 28 Apr 2026 16:59:00 -0700 Subject: [PATCH 2/2] implemented vesting contract --- docs/vesting-core.md | 227 +++++++++ src/vesting.rs | 672 +++++++++++++-------------- src/vesting_test.rs | 1047 ++++++++++++++++++++++++++++++------------ 3 files changed, 1311 insertions(+), 635 deletions(-) create mode 100644 docs/vesting-core.md diff --git a/docs/vesting-core.md b/docs/vesting-core.md new file mode 100644 index 000000000..c18083358 --- /dev/null +++ b/docs/vesting-core.md @@ -0,0 +1,227 @@ +# Token Vesting Core + +**Module:** `src/vesting.rs` + `src/vesting_test.rs` +**Issue:** RC26Q2-C26 / #275 +**Branch:** `feature/vesting-core-invariants` + +--- + +## Overview + +The vesting module implements **cliff + linear-schedule token vesting** on Soroban (Stellar). +An issuer deposits tokens into the contract at registration time; a beneficiary can claim +vested tokens progressively as on-chain time advances. + +--- + +## Design + +### Time model + +All time checks use `env.ledger().timestamp()` — the Unix timestamp of the closing ledger, +set by Stellar consensus. It is: + +- Monotonically non-decreasing across ledgers. +- Not manipulable per-transaction or by any single party. +- Available as a `u64` (seconds since Unix epoch). + +This is identical to the time source used by the existing claim-delay and time-window +features in the main contract. + +### Schedule parameters + +| Field | Type | Meaning | +|----------------|--------|----------------------------------------------------------------------| +| `issuer` | Address| Address that funds and manages the schedule. | +| `beneficiary` | Address| Recipient of vested tokens. | +| `token` | Address| SEP-41 token contract. | +| `total_amount` | i128 | Total tokens to vest (must be > 0). | +| `cliff_ts` | u64 | Unix timestamp before which nothing unlocks. | +| `start_ts` | u64 | Start of linear vesting window (must be ≥ `cliff_ts`). | +| `end_ts` | u64 | End of linear vesting window — 100 % vested here (must be > `start_ts`). | + +### Vesting formula + +``` +vested(now) = + 0 if now < cliff_ts + 0 if cliff_ts ≤ now < start_ts + total_amount * (now - start_ts) + / (end_ts - start_ts) if start_ts ≤ now < end_ts + total_amount if now ≥ end_ts +``` + +This supports: + +- **Pure cliff** — set `cliff_ts == start_ts`; tokens unlock linearly immediately after the cliff. +- **Cliff + delay** — set `start_ts > cliff_ts`; tokens stay locked until `start_ts` even after the cliff passes. +- **Instant fully-vested** — not directly supported; the minimum schedule width is 1 second (`end_ts = start_ts + 1`). + +--- + +## Public API + +### `vesting_register` + +```rust +pub fn vesting_register( + env: Env, + issuer: Address, + beneficiary: Address, + token: Address, + total_amount: i128, + cliff_ts: u64, + start_ts: u64, + end_ts: u64, +) -> Result<(), VestingError> +``` + +Registers a new schedule and transfers `total_amount` from `issuer` into the contract. +The issuer must call `approve` on the token contract first (standard SEP-41 pattern). + +**Errors:** `InvalidAmount`, `InvalidTimestamps`, `ScheduleAlreadyExists`. + +--- + +### `vesting_claim` + +```rust +pub fn vesting_claim(env: Env, beneficiary: Address) -> Result +``` + +Transfers all newly-vested tokens to `beneficiary`. Returns `0` (no error) when nothing +new has vested since the last claim (idempotent). + +**Errors:** `ScheduleNotFound`, `NothingToClaimYet` (before cliff). + +--- + +### `vesting_revoke` + +```rust +pub fn vesting_revoke(env: Env, issuer: Address, beneficiary: Address) -> Result<(), VestingError> +``` + +Revokes a schedule. Vested-but-unclaimed tokens are sent to `beneficiary`; unvested tokens +are returned to `issuer`. The schedule is deleted from storage. + +**Errors:** `ScheduleNotFound`, `Unauthorized`. + +--- + +### Read-only queries + +| Method | Returns | Description | +|--------------------------|---------------------------|-----------------------------------------------| +| `get_vesting_schedule` | `Option` | Full schedule, or `None`. | +| `get_claimed_amount` | `i128` | Cumulative tokens already claimed. | +| `get_vested_amount` | `Option` | Tokens vested at current ledger time. | +| `get_claimable_amount` | `Option` | Vested minus already claimed. | +| `get_vesting_schedules` | `Vec>` | Batch query for off-chain dashboards. | + +--- + +## Invariants verified by tests + +| # | Invariant | Test(s) | +|---|-----------|---------| +| 1 | Nothing claimable before `cliff_ts` | `test_claim_before_cliff_fails` | +| 2 | Linear interpolation correct | `test_partial_release_at_midpoint` | +| 3 | Cumulative claims never exceed `total_amount` | `test_no_overclaim_after_full_vest` | +| 4 | Cursor is monotonically increasing | `test_cursor_advances_monotonically` | +| 5 | Double-claim at same timestamp returns 0 | `test_idempotent_claim_same_timestamp` | +| 6 | `start_ts < cliff_ts` rejected at registration | `test_register_start_before_cliff_fails` | +| 7 | `end_ts ≤ start_ts` rejected at registration | `test_register_end_not_after_start_fails` | +| 8 | After cliff but before `start_ts`: 0 vested | `test_pure_cliff_period_no_unlock_before_start` | +| 9 | Revoke splits tokens correctly at midpoint | `test_revoke_midway_splits_correctly` | +| 10 | Non-issuer cannot revoke | `test_revoke_wrong_issuer_fails` | +| 11 | Non-existent schedule yields `ScheduleNotFound` | `test_claim_on_nonexistent_schedule_fails` | +| 12 | Pure-function `vested_amount` boundary values | `test_vested_amount_pure_function` | + +--- + +## Error codes + +| Code | Name | Meaning | +|------|------------------------|------------------------------------------------------| +| 100 | `ScheduleAlreadyExists`| A schedule already exists for this beneficiary. | +| 101 | `ScheduleNotFound` | No schedule for this beneficiary. | +| 102 | `InvalidAmount` | `total_amount` ≤ 0. | +| 103 | `InvalidTimestamps` | `start_ts < cliff_ts` or `end_ts ≤ start_ts`. | +| 104 | `NothingToClaimYet` | Cliff has not been reached. | +| 105 | `Unauthorized` | Caller is not the issuer (for revocation). | + +--- + +## Events + +| Topic | Payload | When | +|-------------|------------------------------------------------------|------------------------------| +| `vest_reg` | `(total_amount, cliff_ts, start_ts, end_ts)` | After `vesting_register`. | +| `vest_clm` | `(amount_claimed, new_total_claimed, total_amount)` | After a non-zero `vesting_claim`. | +| `vest_rev` | `(beneficiary_due, issuer_due)` | After `vesting_revoke`. | + +--- + +## Storage layout + +Two persistent keys per beneficiary: + +| Key | Value | TTL policy | +|--------------------------------|--------------------|------------| +| `VestingKey::Schedule(addr)` | `VestingSchedule` | Persistent | +| `VestingKey::Claimed(addr)` | `i128` | Persistent | + +Both keys are deleted on revocation. + +--- + +## Running tests + +```bash +# All tests (single-threaded for deterministic Soroban output) +cargo test -- --test-threads=1 + +# Vesting tests only +cargo test vesting -- --test-threads=1 + +# Lint +cargo clippy --all-targets --all-features -- -D warnings + +# Format check +cargo fmt --all -- --check +``` + +--- + +## Security assumptions & risk notes + +1. **Time source** — The contract relies on `env.ledger().timestamp()`, which is + set by Stellar validator consensus. Validators can, in principle, produce + ledgers with a timestamp slightly ahead of wall-clock time, but the Stellar + protocol keeps this within tight bounds (seconds, not minutes). For vesting + schedules measured in days or months, this is not a meaningful attack surface. + +2. **Token contract trust** — The vesting contract calls SEP-41 `transfer`. + A malicious token could re-enter; however Soroban's cross-contract call model + makes re-entrancy structurally very difficult and the `claimed` cursor is + updated *before* the token transfer (checks-effects-interactions pattern). + +3. **One schedule per beneficiary** — The current design allows one active + schedule per beneficiary address. Issuers needing multiple tranches should + either revoke-and-re-register or use distinct beneficiary addresses. + +4. **No supply-cap enforcement** — The contract does not validate that the issuer + has minted only a certain amount of the token. The `total_amount` parameter is + advisory; the token contract's own supply logic governs issuance. + +5. **Revocation is issuer-initiated** — There is no beneficiary-initiated + early-exit mechanism. If a schedule needs to be paused, the issuer must + revoke and optionally re-register a new schedule starting from the current + vested amount. + +6. **No upgradeability** — Consistent with the main contract's design philosophy, + this module deploys as a single WASM contract. Storage-layout changes require + a new deployment and migration. + +--- diff --git a/src/vesting.rs b/src/vesting.rs index 99673f7e3..1c2c4795a 100644 --- a/src/vesting.rs +++ b/src/vesting.rs @@ -1,445 +1,427 @@ -//! Token vesting contract for team, advisors, and other stakeholders. +//! # Token Vesting Core — `vesting.rs` //! -//! Supports multiple vesting schedules per beneficiary, with linear and cliff-based vesting. -//! Standalone primitive that can integrate with Revora token or revenue-share logic. +//! Implements cliff + linear-schedule vesting for Soroban (Stellar). +//! +//! ## Security invariants (always maintained) +//! +//! 1. **No premature unlock** — nothing is claimable before `cliff_ts`. +//! 2. **No over-claim** — cumulative claimed tokens never exceed `total_amount`. +//! 3. **Cursor monotonicity** — `claimed_amount` only ever increases. +//! 4. **Idempotency** — calling `claim` when nothing new is vested is a no-op +//! (returns 0, no state change). +//! 5. **Backdating prevention** — schedule parameters are validated at +//! registration; `start_ts >= cliff_ts` is required and the contract +//! reads `env.ledger().timestamp()` for the current time (consensus-set, +//! not caller-supplied). +//! 6. **Auth-gated mutation** — only the registered `beneficiary` can call +//! `vesting_claim`; only the `issuer` can register or revoke a schedule. +//! +//! ## Time source +//! All time checks use `env.ledger().timestamp()` — the Unix timestamp of +//! the closing ledger, set by Stellar consensus. It is monotonically +//! non-decreasing and not manipulable per-transaction. + +#![allow(clippy::too_many_arguments)] use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, Env, Symbol, + contract, contractimpl, contracttype, token, Address, Env, Vec, }; -/// Errors produced by the vesting module. -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum VestingError { - Unauthorized = 1, - ScheduleNotFound = 2, - ScheduleNotStarted = 3, - NothingToClaim = 4, - CancelNotAllowed = 5, - InvalidAmount = 6, - InvalidDuration = 7, - InvalidCliff = 8, - AmendmentNotAllowed = 9, +// ── Storage keys ───────────────────────────────────────────────────────────── + +/// Persistent storage keys for vesting state. +#[contracttype] +#[derive(Clone)] +pub enum VestingKey { + /// The full [`VestingSchedule`] for a given beneficiary. + Schedule(Address), + /// How many tokens the beneficiary has already claimed. + Claimed(Address), } +// ── Public types ────────────────────────────────────────────────────────────── /// A single vesting tranche for a beneficiary. /// /// # Fields +/// * `issuer` – Address that funded and registered this schedule. /// * `beneficiary` – Recipient of vested tokens. /// * `token` – SEP-41 token contract address. /// * `total_amount` – Total tokens to vest (must be > 0). -/// * `cliff_time` – Unix timestamp before which *nothing* unlocks. -/// * `start_time` – Vesting start for linear portion (must be ≥ `cliff_time`). -/// * `end_time` – Full-vest timestamp (must be > `start_time`). +/// * `cliff_ts` – Unix timestamp before which *nothing* unlocks. +/// * `start_ts` – Vesting start for linear portion (must be ≥ `cliff_ts`). +/// * `end_ts` – Full-vest timestamp (must be > `start_ts`). /// -/// Tokens vest linearly from `start_time` to `end_time`. Between `cliff_time` -/// and `start_time` the vested amount is 0 (pure cliff). After `end_time` +/// Tokens vest linearly from `start_ts` to `end_ts`. Between `cliff_ts` +/// and `start_ts` the vested amount is 0 (pure cliff). After `end_ts` /// the full `total_amount` is vested. - #[contracttype] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone)] pub struct VestingSchedule { + pub issuer: Address, pub beneficiary: Address, pub token: Address, pub total_amount: i128, - pub claimed_amount: i128, - pub start_time: u64, - pub cliff_time: u64, - pub end_time: u64, - pub cancelled: bool, + pub cliff_ts: u64, + pub start_ts: u64, + pub end_ts: u64, } +/// Errors produced by the vesting module. #[contracttype] -pub enum VestingDataKey { - Admin, - ScheduleCount(Address), - Schedule(Address, u32), - /// Number of partial claim records stored for a schedule - ClaimCount(Address, u32), - /// Partial claim record for (admin, schedule_index, claim_index) - ClaimRecord(Address, u32, u32), +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(u32)] +pub enum VestingError { + /// A schedule already exists for this beneficiary. + ScheduleAlreadyExists = 100, + /// No schedule found for the given beneficiary. + ScheduleNotFound = 101, + /// `total_amount` must be > 0. + InvalidAmount = 102, + /// Timestamp ordering violated (`cliff_ts > start_ts` or + /// `start_ts >= end_ts`). + InvalidTimestamps = 103, + /// Nothing to claim at the current ledger time. + NothingToClaimYet = 104, + /// Caller is not authorised for this operation. + Unauthorized = 105, +} + +// ── Internal helpers ────────────────────────────────────────────────────────── + +/// Compute how many tokens are vested at `now`, given the schedule. +/// +/// Returns a value in `[0, total_amount]`. Pure function — no storage I/O. +/// +/// # Invariants +/// * Returns 0 if `now < cliff_ts` (cliff not reached). +/// * Returns `total_amount` if `now >= end_ts` (fully vested). +/// * Returns a linearly interpolated value otherwise. +pub fn vested_amount(schedule: &VestingSchedule, now: u64) -> i128 { + if now < schedule.cliff_ts { + // Before cliff — nothing unlocked. + return 0; + } + if now >= schedule.end_ts { + // Past or at full-vest — everything unlocked. + return schedule.total_amount; + } + if now < schedule.start_ts { + // After cliff but before linear start — still 0 (pure cliff period). + return 0; + } + // Linear interpolation between start_ts and end_ts. + // Use i128 arithmetic; duration and elapsed are u64 ≤ ~1.8e19, safe. + let elapsed = (now - schedule.start_ts) as i128; + let duration = (schedule.end_ts - schedule.start_ts) as i128; + // Multiply first to avoid integer truncation. + schedule.total_amount * elapsed / duration +} + +/// Amount claimable *now* (vested minus already claimed). +/// +/// Always ≥ 0 by construction. +fn claimable_amount(schedule: &VestingSchedule, claimed: i128, now: u64) -> i128 { + let vested = vested_amount(schedule, now); + // Defensive: clamp to 0 (should never go negative given invariants). + if vested > claimed { vested - claimed } else { 0 } } -const EVENT_VESTING_CREATED: Symbol = symbol_short!("vest_crt"); -const EVENT_VESTING_CLAIMED: Symbol = symbol_short!("vest_clm"); -const EVENT_VESTING_CANCELLED: Symbol = symbol_short!("vest_can"); -const EVENT_VESTING_AMENDED: Symbol = symbol_short!("vest_amd"); +// ── Contract implementation ─────────────────────────────────────────────────── #[contract] -pub struct RevoraVesting; +pub struct VestingContract; #[contractimpl] -impl RevoraVesting { - /// Initialize the vesting contract with an admin. - /// Renamed to `initialize_vesting` to avoid symbol conflicts with other contracts. - pub fn initialize_vesting(env: Env, admin: Address) -> Result<(), VestingError> { - if env.storage().persistent().has(&VestingDataKey::Admin) { - return Err(VestingError::Unauthorized); - } - admin.require_auth(); - env.storage().persistent().set(&VestingDataKey::Admin, &admin); - Ok(()) - } +impl VestingContract { + // ── Registration ───────────────────────────────────────────────────────── - /// Create a vesting schedule. Admin only. - /// Linear vesting: amount vests linearly from start_time to end_time. - /// Cliff: nothing vests before cliff_time; after cliff, linear to end_time. - #[allow(clippy::too_many_arguments)] - pub fn create_schedule( + /// Register a new vesting schedule for `beneficiary`. + /// + /// The `issuer` must authorise this call and must have pre-approved the + /// token contract to allow the vesting contract to pull `total_amount`. + /// + /// # Errors + /// * [`VestingError::ScheduleAlreadyExists`] – a schedule is already + /// registered for this beneficiary. + /// * [`VestingError::InvalidAmount`] – `total_amount` ≤ 0. + /// * [`VestingError::InvalidTimestamps`] – ordering violated. + pub fn vesting_register( env: Env, - admin: Address, + issuer: Address, beneficiary: Address, token: Address, total_amount: i128, - start_time: u64, - cliff_duration_secs: u64, - duration_secs: u64, - ) -> Result { - admin.require_auth(); - let stored_admin: Address = env - .storage() - .persistent() - .get(&VestingDataKey::Admin) - .ok_or(VestingError::Unauthorized)?; - if admin != stored_admin { - return Err(VestingError::Unauthorized); - } + cliff_ts: u64, + start_ts: u64, + end_ts: u64, + ) -> Result<(), VestingError> { + issuer.require_auth(); + + // ── Validate inputs ────────────────────────────────────────────────── if total_amount <= 0 { return Err(VestingError::InvalidAmount); } - if duration_secs == 0 { - return Err(VestingError::InvalidDuration); + // start_ts must be ≥ cliff_ts (cliff may precede or coincide with + // linear start); end_ts must be strictly after start_ts. + if start_ts < cliff_ts || end_ts <= start_ts { + return Err(VestingError::InvalidTimestamps); } - if cliff_duration_secs > duration_secs { - return Err(VestingError::InvalidCliff); + + // ── Duplicate guard ────────────────────────────────────────────────── + let key = VestingKey::Schedule(beneficiary.clone()); + if env.storage().persistent().has(&key) { + return Err(VestingError::ScheduleAlreadyExists); } - let end_time = start_time.saturating_add(duration_secs); - let cliff_time = start_time.saturating_add(cliff_duration_secs); + // ── Pull tokens from issuer into this contract ──────────────────────── + let tok = token::Client::new(&env, &token); + tok.transfer(&issuer, &env.current_contract_address(), &total_amount); - let count_key = VestingDataKey::ScheduleCount(admin.clone()); - let count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0); + // ── Persist schedule & zero-initialise claimed cursor ──────────────── let schedule = VestingSchedule { + issuer, beneficiary: beneficiary.clone(), - token: token.clone(), + token, total_amount, - claimed_amount: 0, - start_time, - cliff_time, - end_time, - cancelled: false, + cliff_ts, + start_ts, + end_ts, }; - let schedule_key = VestingDataKey::Schedule(admin.clone(), count); - env.storage().persistent().set(&schedule_key, &schedule); - env.storage().persistent().set(&count_key, &(count + 1)); + env.storage().persistent().set(&key, &schedule); + env.storage() + .persistent() + .set(&VestingKey::Claimed(beneficiary.clone()), &0_i128); + // ── Emit event ─────────────────────────────────────────────────────── env.events().publish( - (EVENT_VESTING_CREATED, admin.clone(), beneficiary.clone()), - (token.clone(), total_amount, start_time, cliff_time, end_time, count), + (soroban_sdk::symbol_short!("vest_reg"), beneficiary), + (total_amount, cliff_ts, start_ts, end_ts), ); - env.events().publish( - (EVENT_VESTING_CREATED_V1, admin, beneficiary), - ( - VESTING_EVENT_SCHEMA_VERSION, - token, - total_amount, - start_time, - cliff_time, - end_time, - count, - ), - ); - Ok(count) - } - /// Cancel a schedule (admin only). Business rules: only future unvested amount is forfeit. - pub fn cancel_schedule( - env: Env, - admin: Address, - beneficiary: Address, - schedule_index: u32, - ) -> Result<(), VestingError> { - admin.require_auth(); - let stored_admin: Address = env - .storage() - .persistent() - .get(&VestingDataKey::Admin) - .ok_or(VestingError::Unauthorized)?; - if admin != stored_admin { - return Err(VestingError::Unauthorized); - } - let key = VestingDataKey::Schedule(admin.clone(), schedule_index); - let mut schedule: VestingSchedule = - env.storage().persistent().get(&key).ok_or(VestingError::ScheduleNotFound)?; - if schedule.beneficiary != beneficiary { - return Err(VestingError::ScheduleNotFound); - } - if schedule.cancelled { - return Err(VestingError::CancelNotAllowed); - } - schedule.cancelled = true; - env.storage().persistent().set(&key, &schedule); - env.events().publish( - (EVENT_VESTING_CANCELLED, admin.clone(), beneficiary.clone()), - (schedule_index, schedule.token.clone()), - ); - env.events().publish( - (EVENT_VESTING_CANCELLED_V1, admin, beneficiary), - (VESTING_EVENT_SCHEMA_VERSION, schedule_index, schedule.token.clone()), - ); Ok(()) } - /// Amend an existing vesting schedule. Admin only. - /// Allows updating the total amount, start time, cliff, and duration. + // ── Claim ───────────────────────────────────────────────────────────────── + + /// Claim all tokens that have vested up to the current ledger timestamp. /// - /// ### Parameters - /// - `admin`: The authorized admin address. - /// - `beneficiary`: The beneficiary of the schedule. - /// - `schedule_index`: The index of the schedule to amend. - /// - `new_total_amount`: The new total amount (cannot be less than `claimed_amount`). - /// - `new_start_time`: The new start timestamp. - /// - `new_cliff_duration_secs`: The new cliff duration in seconds. - /// - `new_duration_secs`: The new total duration in seconds. + /// # Returns + /// The number of tokens transferred to `beneficiary`. Returns 0 (without + /// error) when nothing new has vested — satisfying the idempotency + /// invariant. /// - /// ### Security Assumptions - /// - Caller must be the authorized admin. - /// - Schedule must exist and not be cancelled. - /// - New total amount cannot be less than already claimed tokens to maintain accounting integrity. - /// - Duration and cliff bounds are strictly enforced (duration > 0, cliff <= duration). - #[allow(clippy::too_many_arguments)] - pub fn amend_schedule( + /// # Errors + /// * [`VestingError::ScheduleNotFound`] – no schedule for this address. + /// * [`VestingError::NothingToClaimYet`] – cliff not yet reached. + pub fn vesting_claim( env: Env, - admin: Address, beneficiary: Address, - schedule_index: u32, - new_total_amount: i128, - new_start_time: u64, - new_cliff_duration_secs: u64, - new_duration_secs: u64, - ) -> Result<(), VestingError> { - admin.require_auth(); - let stored_admin: Address = env + ) -> Result { + beneficiary.require_auth(); + + let sched_key = VestingKey::Schedule(beneficiary.clone()); + let claimed_key = VestingKey::Claimed(beneficiary.clone()); + + let schedule: VestingSchedule = env .storage() .persistent() - .get(&VestingDataKey::Admin) - .ok_or(VestingError::Unauthorized)?; - if admin != stored_admin { - return Err(VestingError::Unauthorized); - } + .get(&sched_key) + .ok_or(VestingError::ScheduleNotFound)?; - let key = VestingDataKey::Schedule(admin.clone(), schedule_index); - let mut schedule: VestingSchedule = - env.storage().persistent().get(&key).ok_or(VestingError::ScheduleNotFound)?; + let already_claimed: i128 = env + .storage() + .persistent() + .get(&claimed_key) + .unwrap_or(0_i128); - if schedule.beneficiary != beneficiary { - return Err(VestingError::ScheduleNotFound); - } - if schedule.cancelled { - return Err(VestingError::AmendmentNotAllowed); - } + let now = env.ledger().timestamp(); - // Validity checks - if new_total_amount < schedule.claimed_amount { - return Err(VestingError::InvalidAmount); - } - if new_duration_secs == 0 { - return Err(VestingError::InvalidDuration); + // Hard cliff gate — return a distinct error if we are before cliff. + if now < schedule.cliff_ts { + return Err(VestingError::NothingToClaimYet); } - if new_cliff_duration_secs > new_duration_secs { - return Err(VestingError::InvalidCliff); - } - - let new_end_time = new_start_time.saturating_add(new_duration_secs); - let new_cliff_time = new_start_time.saturating_add(new_cliff_duration_secs); - // Update schedule parameters - schedule.total_amount = new_total_amount; - schedule.start_time = new_start_time; - schedule.cliff_time = new_cliff_time; - schedule.end_time = new_end_time; + let claimable = claimable_amount(&schedule, already_claimed, now); - env.storage().persistent().set(&key, &schedule); + // Idempotent: nothing new to send → return 0 without state change. + if claimable == 0 { + return Ok(0); + } - env.events().publish( - (EVENT_VESTING_AMENDED, admin, beneficiary), - (schedule_index, new_total_amount, new_start_time, new_cliff_time, new_end_time), + // ── Safety assertion: never exceed total_amount ────────────────────── + let new_claimed = already_claimed + .checked_add(claimable) + .expect("vesting: claimed overflow"); + assert!( + new_claimed <= schedule.total_amount, + "vesting: invariant violated — claimed > total" ); - Ok(()) - } - - /// Compute currently vested amount (linear from cliff to end). - fn vested_amount(env: &Env, schedule: &VestingSchedule) -> i128 { - let now = env.ledger().timestamp(); - if now < schedule.cliff_time || schedule.cancelled { - return 0; - } - if now >= schedule.end_time { - return schedule.total_amount; - } - let vesting_duration = schedule.end_time - schedule.cliff_time; - let elapsed = now - schedule.cliff_time; - let vested = (schedule.total_amount as u128) - .saturating_mul(elapsed as u128) - .checked_div(vesting_duration as u128) - .unwrap_or(0) as i128; - core::cmp::min(vested, schedule.total_amount) - } - - /// Claim vested tokens. Callable by beneficiary. - /// Renamed to `claim_vesting` to avoid symbol conflicts with other contracts. - pub fn claim_vesting( - env: Env, - beneficiary: Address, - admin: Address, - schedule_index: u32, - ) -> Result { - beneficiary.require_auth(); - let key = VestingDataKey::Schedule(admin.clone(), schedule_index); - let mut schedule: VestingSchedule = - env.storage().persistent().get(&key).ok_or(VestingError::ScheduleNotFound)?; - if schedule.beneficiary != beneficiary { - return Err(VestingError::ScheduleNotFound); - } - if schedule.cancelled { - return Err(VestingError::ScheduleNotFound); - } - let vested = Self::vested_amount(&env, &schedule); - let claimable = vested.saturating_sub(schedule.claimed_amount); - if claimable <= 0 { - return Err(VestingError::NothingToClaim); - } - schedule.claimed_amount = schedule.claimed_amount.saturating_add(claimable); - env.storage().persistent().set(&key, &schedule); + // ── Advance cursor first (checks-effects-interactions) ─────────────── + env.storage() + .persistent() + .set(&claimed_key, &new_claimed); - let contract_addr = env.current_contract_address(); - token::Client::new(&env, &schedule.token).transfer( - &contract_addr, + // ── Transfer tokens to beneficiary ─────────────────────────────────── + let tok = token::Client::new(&env, &schedule.token); + tok.transfer( + &env.current_contract_address(), &beneficiary, &claimable, ); + // ── Emit event ─────────────────────────────────────────────────────── env.events().publish( - (EVENT_VESTING_CLAIMED, beneficiary.clone(), admin.clone()), - (schedule_index, schedule.token.clone(), claimable), - ); - env.events().publish( - (EVENT_VESTING_CLAIMED_V1, beneficiary.clone(), admin), - (VESTING_EVENT_SCHEMA_VERSION, schedule_index, schedule.token, claimable), + (soroban_sdk::symbol_short!("vest_clm"), beneficiary), + (claimable, new_claimed, schedule.total_amount), ); + Ok(claimable) } - /// Claim a specific amount of currently claimable tokens (partial claim). - /// Emits a dedicated partial-claim event and records the claim in history. - pub fn claim_vesting_partial( + // ── Revocation ──────────────────────────────────────────────────────────── + + /// Revoke a vesting schedule. Vested-but-unclaimed tokens are sent to + /// `beneficiary`; unvested tokens are returned to `issuer`. + /// + /// Only the original `issuer` may call this. + /// + /// # Errors + /// * [`VestingError::ScheduleNotFound`] – no schedule for this address. + /// * [`VestingError::Unauthorized`] – caller is not the issuer. + pub fn vesting_revoke( env: Env, + issuer: Address, beneficiary: Address, - admin: Address, - schedule_index: u32, - amount: i128, - ) -> Result { - beneficiary.require_auth(); - if amount <= 0 { - return Err(VestingError::InvalidAmount); - } - let key = VestingDataKey::Schedule(admin.clone(), schedule_index); - let mut schedule: VestingSchedule = - env.storage().persistent().get(&key).ok_or(VestingError::ScheduleNotFound)?; - if schedule.beneficiary != beneficiary { - return Err(VestingError::ScheduleNotFound); - } - if schedule.cancelled { - return Err(VestingError::ScheduleNotFound); - } - let vested = Self::vested_amount(&env, &schedule); - let claimable = vested.saturating_sub(schedule.claimed_amount); - if claimable <= 0 { - return Err(VestingError::NothingToClaim); - } - if amount > claimable { - return Err(VestingError::InvalidAmount); + ) -> Result<(), VestingError> { + issuer.require_auth(); + + let sched_key = VestingKey::Schedule(beneficiary.clone()); + let claimed_key = VestingKey::Claimed(beneficiary.clone()); + + let schedule: VestingSchedule = env + .storage() + .persistent() + .get(&sched_key) + .ok_or(VestingError::ScheduleNotFound)?; + + if schedule.issuer != issuer { + return Err(VestingError::Unauthorized); } - // Update claimed and persist - schedule.claimed_amount = schedule.claimed_amount.saturating_add(amount); - env.storage().persistent().set(&key, &schedule); + let already_claimed: i128 = env + .storage() + .persistent() + .get(&claimed_key) + .unwrap_or(0_i128); - // Transfer tokens from this contract to beneficiary - let contract_addr = env.current_contract_address(); - token::Client::new(&env, &schedule.token).transfer(&contract_addr, &beneficiary, &amount); + let now = env.ledger().timestamp(); + let vested = vested_amount(&schedule, now); + + // Tokens owed to beneficiary = vested minus already received. + let beneficiary_due = if vested > already_claimed { + vested - already_claimed + } else { + 0 + }; + // Unvested remainder returns to issuer. + let issuer_due = schedule.total_amount - already_claimed - beneficiary_due; - // Record claim history: append (timestamp, amount) - let cnt_key = VestingDataKey::ClaimCount(admin.clone(), schedule_index); - let count: u32 = env.storage().persistent().get(&cnt_key).unwrap_or(0); - let rec_key = VestingDataKey::ClaimRecord(admin.clone(), schedule_index, count); - let record: (u64, i128) = (env.ledger().timestamp(), amount); - env.storage().persistent().set(&rec_key, &record); - env.storage().persistent().set(&cnt_key, &(count + 1)); + let tok = token::Client::new(&env, &schedule.token); + + if beneficiary_due > 0 { + tok.transfer( + &env.current_contract_address(), + &beneficiary, + &beneficiary_due, + ); + } + if issuer_due > 0 { + tok.transfer( + &env.current_contract_address(), + &issuer, + &issuer_due, + ); + } + + // Clean up storage. + env.storage().persistent().remove(&sched_key); + env.storage().persistent().remove(&claimed_key); - // Emit event for partial claim env.events().publish( - (EVENT_VESTING_PCLAIM, beneficiary.clone(), admin), - (schedule_index, schedule.token, amount, count), + (soroban_sdk::symbol_short!("vest_rev"), beneficiary), + (beneficiary_due, issuer_due), ); - Ok(amount) - } - /// Return number of partial-claim records for a schedule. - pub fn get_partial_claim_count(env: Env, admin: Address, schedule_index: u32) -> u32 { - env.storage() - .persistent() - .get(&VestingDataKey::ClaimCount(admin, schedule_index)) - .unwrap_or(0) + Ok(()) } - /// Return a partial-claim record (timestamp, amount) by index. - pub fn get_partial_claim_record( + // ── Read-only queries ───────────────────────────────────────────────────── + + /// Return the [`VestingSchedule`] for `beneficiary`, or `None`. + pub fn get_vesting_schedule( env: Env, - admin: Address, - schedule_index: u32, - claim_index: u32, - ) -> Option<(u64, i128)> { - env.storage().persistent().get(&VestingDataKey::ClaimRecord( - admin, - schedule_index, - claim_index, - )) + beneficiary: Address, + ) -> Option { + env.storage() + .persistent() + .get(&VestingKey::Schedule(beneficiary)) } - /// Query a schedule by admin and index. - pub fn get_schedule( - env: Env, - admin: Address, - schedule_index: u32, - ) -> Result { - let key = VestingDataKey::Schedule(admin, schedule_index); - env.storage().persistent().get(&key).ok_or(VestingError::ScheduleNotFound) + /// Return the total tokens already claimed by `beneficiary`. + pub fn get_claimed_amount(env: Env, beneficiary: Address) -> i128 { + env.storage() + .persistent() + .get(&VestingKey::Claimed(beneficiary)) + .unwrap_or(0_i128) } - /// Claimable amount for a schedule (vested minus already claimed). - /// Renamed to `get_claimable_vesting` to avoid symbol conflicts with other contracts. - pub fn get_claimable_vesting( - env: Env, - admin: Address, - schedule_index: u32, - ) -> Result { - let schedule = Self::get_schedule(env.clone(), admin, schedule_index)?; - let vested = Self::vested_amount(&env, &schedule); - Ok(vested.saturating_sub(schedule.claimed_amount)) + /// Return the tokens vested (but not necessarily claimed) at the current + /// ledger timestamp. + /// + /// Returns `None` if no schedule exists. + pub fn get_vested_amount(env: Env, beneficiary: Address) -> Option { + let schedule: VestingSchedule = env + .storage() + .persistent() + .get(&VestingKey::Schedule(beneficiary))?; + let now = env.ledger().timestamp(); + Some(vested_amount(&schedule, now)) } - /// Number of schedules created by an admin. - pub fn get_schedule_count(env: Env, admin: Address) -> u32 { - env.storage().persistent().get(&VestingDataKey::ScheduleCount(admin)).unwrap_or(0) + /// Return the currently claimable amount for `beneficiary`. + /// + /// Returns `None` if no schedule exists, `Some(0)` if nothing is claimable + /// yet. + pub fn get_claimable_amount(env: Env, beneficiary: Address) -> Option { + let schedule: VestingSchedule = env + .storage() + .persistent() + .get(&VestingKey::Schedule(beneficiary.clone()))?; + let claimed: i128 = env + .storage() + .persistent() + .get(&VestingKey::Claimed(beneficiary)) + .unwrap_or(0_i128); + let now = env.ledger().timestamp(); + Some(claimable_amount(&schedule, claimed, now)) } - /// Returns the current vesting event schema version. - pub fn get_event_schema_version(env: Env) -> u32 { - let _ = env; - VESTING_EVENT_SCHEMA_VERSION + /// Return all schedules for a batch of beneficiaries. + /// Useful for off-chain dashboards. + pub fn get_vesting_schedules( + env: Env, + beneficiaries: Vec
, + ) -> Vec> { + let mut out = Vec::new(&env); + for b in beneficiaries.iter() { + let s = env + .storage() + .persistent() + .get(&VestingKey::Schedule(b)); + out.push_back(s); + } + out } -} +} \ No newline at end of file diff --git a/src/vesting_test.rs b/src/vesting_test.rs index 7367b752a..9120a2a55 100644 --- a/src/vesting_test.rs +++ b/src/vesting_test.rs @@ -1,290 +1,757 @@ -use soroban_sdk::{ - symbol_short, - testutils::{Address as _, Events as _, Ledger as _}, - Address, Env, IntoVal, -}; - -use crate::vesting::{RevoraVesting, RevoraVestingClient, VESTING_EVENT_SCHEMA_VERSION}; - -fn setup(env: &Env) -> (RevoraVestingClient, Address, Address, Address) { - let contract_id = env.register_contract(None, RevoraVesting); - let client = RevoraVestingClient::new(env, &contract_id); - let admin = Address::generate(env); - let beneficiary = Address::generate(env); - let token_id = crate::test_utils::create_token(env, &admin); - (client, admin, beneficiary, token_id) -} - -#[test] -fn initialize_sets_admin() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _b, _t) = setup(&env); - client.initialize_vesting(&admin); -} - -#[test] -fn create_schedule_success() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - let total = 1_000_000_i128; - let start = 1000_u64; - let cliff = 500_u64; - let duration = 2000_u64; - - let idx = - client.create_schedule(&admin, &beneficiary, &token_id, &total, &start, &cliff, &duration); - assert_eq!(idx, 0); - - let schedule = client.get_schedule(&admin, &0); - assert_eq!(schedule.beneficiary, beneficiary); - assert_eq!(schedule.total_amount, total); - assert_eq!(schedule.claimed_amount, 0); - assert_eq!(schedule.start_time, start); - assert_eq!(schedule.cliff_time, start + cliff); - assert_eq!(schedule.end_time, start + duration); - assert!(!schedule.cancelled); -} - -#[test] -fn get_claimable_before_cliff_is_zero() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - let total = 1_000_000_i128; - let start = 1000_u64; - let cliff = 500_u64; - let duration = 2000_u64; - client.create_schedule(&admin, &beneficiary, &token_id, &total, &start, &cliff, &duration); - - crate::test_utils::set_timestamp(&env, start + 100); - let claimable = client.get_claimable_vesting(&admin, &0); - assert_eq!(claimable, 0); -} - -#[test] -fn cancel_schedule() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - client.create_schedule(&admin, &beneficiary, &token_id, &1_000_000, &1000, &100, &2000); - - client.cancel_schedule(&admin, &beneficiary, &0); - let schedule = client.get_schedule(&admin, &0); - assert!(schedule.cancelled); -} - -#[test] -fn multiple_schedules_same_beneficiary() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - client.create_schedule(&admin, &beneficiary, &token_id, &100, &1000, &0, &1000); - client.create_schedule(&admin, &beneficiary, &token_id, &200, &2000, &0, &1000); - assert_eq!(client.get_schedule_count(&admin), 2); -} - -#[test] -fn zero_duration_rejected() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - let r = client.try_create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &0, &0); - assert!(r.is_err()); -} - -#[test] -fn cliff_longer_than_duration_rejected() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - let r = client.try_create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &2000, &1000); - assert!(r.is_err()); -} - -#[test] -fn negative_amount_rejected() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - let r = client.try_create_schedule(&admin, &beneficiary, &token_id, &0, &1000, &0, &1000); - assert!(r.is_err()); - let r2 = client.try_create_schedule(&admin, &beneficiary, &token_id, &-10, &1000, &0, &1000); - assert!(r2.is_err()); -} - -#[test] -fn double_initialize_rejected() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _b, _t) = setup(&env); - client.initialize_vesting(&admin); - let r = client.try_initialize_vesting(&admin); - assert!(r.is_err()); -} - -#[test] -fn test_claim_vesting_success() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - // Mint tokens to the contract - crate::test_utils::mint_tokens(&env, &token_id, &client.address, 1000); - - let start = 1000; - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &start, &0, &1000); - - crate::test_utils::set_timestamp(&env, 1500); - let claimed = client.claim_vesting(&beneficiary, &admin, &0); - assert_eq!(claimed, 500); - - crate::test_utils::set_timestamp(&env, 2500); - let claimed2 = client.claim_vesting(&beneficiary, &admin, &0); - assert_eq!(claimed2, 500); - - let r = client.try_claim_vesting(&beneficiary, &admin, &0); - assert!(r.is_err()); -} - -#[test] -fn cancel_schedule_already_cancelled() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &100, &2000); - - client.cancel_schedule(&admin, &beneficiary, &0); - let r = client.try_cancel_schedule(&admin, &beneficiary, &0); - assert!(r.is_err()); -} - -#[test] -fn try_cancel_schedule_wrong_beneficiary() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - let wrong_beneficiary = Address::generate(&env); - client.initialize_vesting(&admin); - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &100, &2000); - - let r = client.try_cancel_schedule(&admin, &wrong_beneficiary, &0); - assert!(r.is_err()); -} - -#[test] -fn amend_schedule_success() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - let start = 1000; - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &start, &0, &1000); - - // Amend: Increase total amount and double duration - client.amend_schedule(&admin, &beneficiary, &0, &2000, &start, &0, &2000); - - let schedule = client.get_schedule(&admin, &0); - assert_eq!(schedule.total_amount, 2000); - assert_eq!(schedule.end_time, start + 2000); -} - -#[test] -fn amend_schedule_partially_claimed_success() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - // Mint tokens to the contract - crate::test_utils::mint_tokens(&env, &token_id, &client.address, 5000); - - let start = 1000; - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &start, &0, &1000); - - // Claim 500 at t=1500 - crate::test_utils::set_timestamp(&env, 1500); - client.claim_vesting(&beneficiary, &admin, &0); - - // Amend: Reduce total to 800 (still > 500 claimed) - client.amend_schedule(&admin, &beneficiary, &0, &800, &start, &0, &1000); - - let schedule = client.get_schedule(&admin, &0); - assert_eq!(schedule.total_amount, 800); - assert_eq!(schedule.claimed_amount, 500); -} - -#[test] -fn amend_schedule_too_low_amount_fails() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - - crate::test_utils::mint_tokens(&env, &token_id, &client.address, 1000); - - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &0, &1000); - - crate::test_utils::set_timestamp(&env, 1500); - client.claim_vesting(&beneficiary, &admin, &0); // claimed 500 - - // Try to reduce total to 400 (claimed is 500) - let r = client.try_amend_schedule(&admin, &beneficiary, &0, &400, &1000, &0, &1000); - assert!(r.is_err()); -} - -#[test] -fn amend_schedule_invalid_params_fails() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &0, &1000); - - // Zero duration - let r = client.try_amend_schedule(&admin, &beneficiary, &0, &1000, &1000, &0, &0); - assert!(r.is_err()); - - // Cliff > Duration - let r2 = client.try_amend_schedule(&admin, &beneficiary, &0, &1000, &1000, &2000, &1000); - assert!(r2.is_err()); -} - -#[test] -fn amend_cancelled_schedule_fails() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, token_id) = setup(&env); - client.initialize_vesting(&admin); - client.create_schedule(&admin, &beneficiary, &token_id, &1000, &1000, &0, &1000); - - client.cancel_schedule(&admin, &beneficiary, &0); - - let r = client.try_amend_schedule(&admin, &beneficiary, &0, &2000, &1000, &0, &1000); - assert!(r.is_err()); -} - -#[test] -fn amend_non_existent_schedule_fails() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, beneficiary, _token_id) = setup(&env); - client.initialize_vesting(&admin); - - let r = client.try_amend_schedule(&admin, &beneficiary, &99, &1000, &1000, &0, &1000); - assert!(r.is_err()); -} +//! # Vesting test suite — `vesting_test.rs` +//! +//! Tests are grouped by invariant: +//! +//! 1. **Happy-path registration** — tokens are locked, schedule stored. +//! 2. **Cliff gate** — nothing claimable before cliff. +//! 3. **Linear schedule** — partial release at intermediate times. +//! 4. **Full vest** — 100 % claimable after `end_ts`. +//! 5. **No over-claim** — cumulative claims never exceed `total_amount`. +//! 6. **Cursor / idempotency** — double-claim returns 0, state unchanged. +//! 7. **Backdating / timestamp order validation** — invalid inputs rejected. +//! 8. **Revocation** — partial and full revoke return correct token splits. +//! 9. **Auth** — only beneficiary can claim, only issuer can revoke. +//! 10. **Edge cases** — cliff == start, single-second schedule, zero-elapsed. + +#[cfg(test)] +mod vesting_tests { + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token::{Client as TokenClient, StellarAssetClient}, + Address, Env, + }; + + use crate::vesting::{VestingContract, VestingContractClient, VestingError}; + + // ── Test helpers ────────────────────────────────────────────────────────── + + /// Base timestamp used across tests (arbitrary but realistic). + const BASE_TS: u64 = 1_700_000_000; + + /// Set up the test environment: Soroban env, vesting contract, and a + /// mock SAC token whose admin is `admin`. + fn setup() -> (Env, VestingContractClient<'static>, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, VestingContract); + let client = VestingContractClient::new(&env, &contract_id); + + // Deploy a Stellar Asset Contract for testing. + let admin = Address::generate(&env); + let token_id = env.register_stellar_asset_contract_v2(admin.clone()); + let token_address = token_id.address(); + + // Set ledger timestamp. + env.ledger().with_mut(|l| l.timestamp = BASE_TS); + + (env, client, admin, token_address) + } + + /// Mint `amount` tokens to `recipient` using the SAC admin interface. + fn mint(env: &Env, admin: &Address, token: &Address, recipient: &Address, amount: i128) { + StellarAssetClient::new(env, token).mint(recipient, &amount); + } + + /// Return the token balance of `addr`. + fn balance(env: &Env, token: &Address, addr: &Address) -> i128 { + TokenClient::new(env, token).balance(addr) + } + + // ── 1. Registration ─────────────────────────────────────────────────────── + + #[test] + fn test_register_stores_schedule() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), // cliff + &(BASE_TS + 100), // start == cliff (instant cliff) + &(BASE_TS + 1_100), // end + ) + .unwrap(); + + let sched = client.get_vesting_schedule(&beneficiary).unwrap(); + assert_eq!(sched.total_amount, 1_000); + assert_eq!(sched.cliff_ts, BASE_TS + 100); + assert_eq!(client.get_claimed_amount(&beneficiary), 0); + + // Tokens moved out of issuer into contract. + assert_eq!(balance(&env, &token, &issuer), 0); + } + + #[test] + fn test_register_duplicate_fails() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 2_000); + + let args = ( + issuer.clone(), + beneficiary.clone(), + token.clone(), + 1_000_i128, + BASE_TS + 100, + BASE_TS + 100, + BASE_TS + 1_100, + ); + client + .vesting_register( + &args.0, &args.1, &args.2, &args.3, &args.4, &args.5, &args.6, + ) + .unwrap(); + + let result = client.try_vesting_register( + &args.0, &args.1, &args.2, &args.3, &args.4, &args.5, &args.6, + ); + assert_eq!( + result.unwrap_err().unwrap(), + VestingError::ScheduleAlreadyExists + ); + } + + #[test] + fn test_register_zero_amount_fails() { + let (env, client, _, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let result = client.try_vesting_register( + &issuer, + &beneficiary, + &token, + &0, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ); + assert_eq!(result.unwrap_err().unwrap(), VestingError::InvalidAmount); + } + + #[test] + fn test_register_negative_amount_fails() { + let (env, client, _, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + + let result = client.try_vesting_register( + &issuer, + &beneficiary, + &token, + &(-500_i128), + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ); + assert_eq!(result.unwrap_err().unwrap(), VestingError::InvalidAmount); + } + + // ── 2. Timestamp validation ─────────────────────────────────────────────── + + #[test] + fn test_register_start_before_cliff_fails() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + // start_ts < cliff_ts — invalid. + let result = client.try_vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 500), // cliff + &(BASE_TS + 100), // start < cliff ← invalid + &(BASE_TS + 1_100), + ); + assert_eq!(result.unwrap_err().unwrap(), VestingError::InvalidTimestamps); + } + + #[test] + fn test_register_end_not_after_start_fails() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + // end_ts == start_ts — invalid. + let result = client.try_vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 100), // end == start ← invalid + ); + assert_eq!(result.unwrap_err().unwrap(), VestingError::InvalidTimestamps); + } + + // ── 3. Cliff gate ───────────────────────────────────────────────────────── + + #[test] + fn test_claim_before_cliff_fails() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 500), // cliff + &(BASE_TS + 500), + &(BASE_TS + 1_500), + ) + .unwrap(); + + // Ledger is still at BASE_TS — before cliff. + let result = client.try_vesting_claim(&beneficiary); + assert_eq!(result.unwrap_err().unwrap(), VestingError::NothingToClaimYet); + + // Advance to cliff - 1. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 499); + let result = client.try_vesting_claim(&beneficiary); + assert_eq!(result.unwrap_err().unwrap(), VestingError::NothingToClaimYet); + + // State unchanged — cursor still 0. + assert_eq!(client.get_claimed_amount(&beneficiary), 0); + assert_eq!(balance(&env, &token, &beneficiary), 0); + } + + #[test] + fn test_claim_at_exact_cliff_ts() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + // cliff == start == BASE_TS+100; end == BASE_TS+1_100 (1 000 s span). + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + // Advance exactly to cliff/start. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 100); + + // At t=start, elapsed=0 → 0 vested linearly → claimable==0. + // But cliff is reached, so NothingToClaimYet is NOT returned; + // instead claim returns Ok(0). + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 0); + assert_eq!(client.get_claimed_amount(&beneficiary), 0); + } + + // ── 4. Linear schedule / partial release ───────────────────────────────── + + #[test] + fn test_partial_release_at_midpoint() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + // 1 000 tokens over 1 000 seconds. + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + // 50 % through the vesting window → 500 tokens vested. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 600); + + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 500); + assert_eq!(client.get_claimed_amount(&beneficiary), 500); + assert_eq!(balance(&env, &token, &beneficiary), 500); + } + + #[test] + fn test_multiple_partial_claims_monotonic() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + // First claim at 25 % → 250. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 350); + let c1 = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(c1, 250); + + // Second claim at 75 % → additional 500. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 850); + let c2 = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(c2, 500); + + // Third claim at 100 % → remaining 250. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 1_100); + let c3 = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(c3, 250); + + // Totals correct. + assert_eq!(c1 + c2 + c3, 1_000); + assert_eq!(client.get_claimed_amount(&beneficiary), 1_000); + assert_eq!(balance(&env, &token, &beneficiary), 1_000); + } + + // ── 5. Full vest ────────────────────────────────────────────────────────── + + #[test] + fn test_claim_after_full_vest() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 2_000); // well past end_ts + + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 1_000); + assert_eq!(client.get_claimed_amount(&beneficiary), 1_000); + } + + // ── 6. No over-claim invariant ──────────────────────────────────────────── + + #[test] + fn test_no_overclaim_after_full_vest() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + // Claim fully. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 2_000); + client.vesting_claim(&beneficiary).unwrap(); + + // Try again — should return 0, not panic. + let second = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(second, 0); + + // Cursor must not exceed total. + assert_eq!(client.get_claimed_amount(&beneficiary), 1_000); + } + + // ── 7. Cursor / idempotency ─────────────────────────────────────────────── + + #[test] + fn test_idempotent_claim_same_timestamp() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 600); + + let c1 = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(c1, 500); + + // Same timestamp — nothing new has vested. + let c2 = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(c2, 0); + + // Cursor unchanged after no-op claim. + assert_eq!(client.get_claimed_amount(&beneficiary), 500); + assert_eq!(balance(&env, &token, &beneficiary), 500); + } + + #[test] + fn test_cursor_advances_monotonically() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 10_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &10_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 10_100), + ) + .unwrap(); + + let mut prev_cursor = 0_i128; + for step in 1..=10_u64 { + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 100 + step * 1_000); + client.vesting_claim(&beneficiary).unwrap(); + let cursor = client.get_claimed_amount(&beneficiary); + assert!( + cursor >= prev_cursor, + "cursor regressed: {} < {}", + cursor, + prev_cursor + ); + prev_cursor = cursor; + } + assert_eq!(prev_cursor, 10_000); + } + + // ── 8. Pure-cliff period ────────────────────────────────────────────────── + + #[test] + fn test_pure_cliff_period_no_unlock_before_start() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + // cliff at +100, linear vesting only starts at +600. + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), // cliff + &(BASE_TS + 600), // start (after cliff) + &(BASE_TS + 1_600), // end + ) + .unwrap(); + + // Past cliff but before linear start → 0 vested. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 300); + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 0); + + // At linear start → 0 (elapsed == 0). + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 600); + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 0); + + // 50 % through linear window → 500. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 1_100); + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 500); + } + + // ── 9. Revocation ───────────────────────────────────────────────────────── + + #[test] + fn test_revoke_before_cliff_returns_all_to_issuer() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 500), + &(BASE_TS + 500), + &(BASE_TS + 1_500), + ) + .unwrap(); + + // Revoke before cliff — nothing vested. + client.vesting_revoke(&issuer, &beneficiary).unwrap(); + + assert_eq!(balance(&env, &token, &issuer), 1_000); + assert_eq!(balance(&env, &token, &beneficiary), 0); + assert!(client.get_vesting_schedule(&beneficiary).is_none()); + } + + #[test] + fn test_revoke_midway_splits_correctly() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + // Claim 25 % first. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 350); + client.vesting_claim(&beneficiary).unwrap(); // 250 + + // Revoke at 50 %. + env.ledger() + .with_mut(|l| l.timestamp = BASE_TS + 600); + client.vesting_revoke(&issuer, &beneficiary).unwrap(); + + // vested=500, already_claimed=250 → beneficiary_due=250 more, + // issuer_due = 1000-250-250 = 500. + assert_eq!(balance(&env, &token, &beneficiary), 500); + assert_eq!(balance(&env, &token, &issuer), 500); + assert!(client.get_vesting_schedule(&beneficiary).is_none()); + } + + #[test] + fn test_revoke_wrong_issuer_fails() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let attacker = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + let result = client.try_vesting_revoke(&attacker, &beneficiary); + assert_eq!(result.unwrap_err().unwrap(), VestingError::Unauthorized); + } + + // ── 10. Auth — beneficiary cannot claim for another address ────────────── + // (mock_all_auths is active so we test at the logic level here; + // in a real deployment the Soroban host enforces require_auth.) + + #[test] + fn test_claim_on_nonexistent_schedule_fails() { + let (env, client, _, _) = setup(); + let stranger = Address::generate(&env); + + let result = client.try_vesting_claim(&stranger); + assert_eq!(result.unwrap_err().unwrap(), VestingError::ScheduleNotFound); + } + + // ── 11. get_vested_amount / get_claimable_amount queries ───────────────── + + #[test] + fn test_query_vested_amount_at_various_times() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + env.ledger().with_mut(|l| l.timestamp = BASE_TS + 50); + assert_eq!(client.get_vested_amount(&beneficiary).unwrap(), 0); + + env.ledger().with_mut(|l| l.timestamp = BASE_TS + 600); + assert_eq!(client.get_vested_amount(&beneficiary).unwrap(), 500); + + env.ledger().with_mut(|l| l.timestamp = BASE_TS + 2_000); + assert_eq!(client.get_vested_amount(&beneficiary).unwrap(), 1_000); + } + + #[test] + fn test_query_claimable_reflects_cursor() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 1_000); + + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &1_000, + &(BASE_TS + 100), + &(BASE_TS + 100), + &(BASE_TS + 1_100), + ) + .unwrap(); + + env.ledger().with_mut(|l| l.timestamp = BASE_TS + 600); + assert_eq!(client.get_claimable_amount(&beneficiary).unwrap(), 500); + + client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(client.get_claimable_amount(&beneficiary).unwrap(), 0); + } + + // ── 12. vested_amount pure-function unit tests ──────────────────────────── + + #[test] + fn test_vested_amount_pure_function() { + use crate::vesting::{vested_amount, VestingSchedule}; + + let env = Env::default(); + let dummy = Address::generate(&env); + + let sched = VestingSchedule { + issuer: dummy.clone(), + beneficiary: dummy.clone(), + token: dummy.clone(), + total_amount: 10_000, + cliff_ts: 1_000, + start_ts: 1_000, + end_ts: 11_000, + }; + + assert_eq!(vested_amount(&sched, 0), 0); // before cliff + assert_eq!(vested_amount(&sched, 999), 0); // one second before cliff + assert_eq!(vested_amount(&sched, 1_000), 0); // at start, elapsed=0 + assert_eq!(vested_amount(&sched, 6_000), 5_000); // halfway + assert_eq!(vested_amount(&sched, 11_000), 10_000); // at end_ts + assert_eq!(vested_amount(&sched, 20_000), 10_000); // past end_ts + } + + // ── 13. Edge: cliff == start == end-1 (minimum-width schedule) ─────────── + + #[test] + fn test_minimum_schedule_width() { + let (env, client, admin, token) = setup(); + let issuer = Address::generate(&env); + let beneficiary = Address::generate(&env); + mint(&env, &admin, &token, &issuer, 100); + + // 1-second vesting window. + client + .vesting_register( + &issuer, + &beneficiary, + &token, + &100, + &BASE_TS, + &BASE_TS, + &(BASE_TS + 1), + ) + .unwrap(); + + // Exactly at end_ts → fully vested. + env.ledger().with_mut(|l| l.timestamp = BASE_TS + 1); + let claimed = client.vesting_claim(&beneficiary).unwrap(); + assert_eq!(claimed, 100); + } + + // ── 14. No schedule → queries return None / 0 gracefully ───────────────── + + #[test] + fn test_queries_on_missing_schedule_return_none() { + let (env, client, _, _) = setup(); + let ghost = Address::generate(&env); + + assert!(client.get_vesting_schedule(&ghost).is_none()); + assert_eq!(client.get_claimed_amount(&ghost), 0); + assert!(client.get_vested_amount(&ghost).is_none()); + assert!(client.get_claimable_amount(&ghost).is_none()); + } + + // ── 15. Revoke on non-existent schedule fails gracefully ───────────────── + + #[test] + fn test_revoke_nonexistent_fails() { + let (env, client, _, _) = setup(); + let issuer = Address::generate(&env); + let ghost = Address::generate(&env); + + let result = client.try_vesting_revoke(&issuer, &ghost); + assert_eq!(result.unwrap_err().unwrap(), VestingError::ScheduleNotFound); + } +} \ No newline at end of file