From 04f20c20550cdb4c2474aeae8b4a0d325ef8fa14 Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 29 May 2026 22:43:52 +0330 Subject: [PATCH 1/2] feat: add Soroban (Stellar) port of DonationHandler Port DonationHandler.sol to a Soroban contract in stellar/. The EVM native-vs-ERC20 split collapses into a single token: Address path via the Stellar Asset Contract, so donate / donate_many handle native XLM and any issued asset (USDC, etc.). Carries the arbitrary data payload (projectId) and emits a DonationMade event (topics: [donation, recipient, token]; data: {amount, data}) for the indexer. - No ReentrancyGuard: Soroban's deterministic auth + stateless design (only calls the host SAC, holds no balances) make it unnecessary. - Upgradeable via admin-gated update_current_contract_wasm (replaces the OZ proxy pattern). - 10 unit tests (soroban-sdk testutils) cover single/batch transfers, event emission, length/sum validation, and admin/upgrade auth. - Verified on testnet: single + 2-recipient batch donations transferred funds and emitted DonationMade with the projectId payload intact. Co-Authored-By: Claude Opus 4.7 (1M context) --- stellar/.gitignore | 4 + stellar/Cargo.lock | 1909 +++++++++++++++++++++++++++++++++++++++++++ stellar/Cargo.toml | 31 + stellar/README.md | 120 +++ stellar/src/lib.rs | 208 +++++ stellar/src/test.rs | 201 +++++ 6 files changed, 2473 insertions(+) create mode 100644 stellar/.gitignore create mode 100644 stellar/Cargo.lock create mode 100644 stellar/Cargo.toml create mode 100644 stellar/README.md create mode 100644 stellar/src/lib.rs create mode 100644 stellar/src/test.rs diff --git a/stellar/.gitignore b/stellar/.gitignore new file mode 100644 index 0000000..3f542bd --- /dev/null +++ b/stellar/.gitignore @@ -0,0 +1,4 @@ +# Rust / Soroban build artifacts +/target +# Soroban test snapshots (regenerated by `cargo test`) +/test_snapshots diff --git a/stellar/Cargo.lock b/stellar/Cargo.lock new file mode 100644 index 0000000..4ac47b3 --- /dev/null +++ b/stellar/Cargo.lock @@ -0,0 +1,1909 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "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 = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.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 = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +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 = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[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 = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[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 = "donation-handler" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[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 = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[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 = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.1", + "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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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 = "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 = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[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.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "serde", + "serde_json", +] + +[[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.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.8.22", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "26.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a3a2b57b132b800e132d2c81e1818359bb2cf787ca39c61c151d6bd0798403" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "26.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c76fad735f9622d8aa0fa0c75838d2023a659a0c57638a783b8b2eb967f7822" +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 = "26.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15aeed6d7a4dc4d3bba65e2ac92f7eeaa664900bbc82e1055e024bf637d74ed3" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "26.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb523456b4efe9cdf869233cff5a2a1a5ebfae8c0acc405e57479c83781a20c" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "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 0.0.13", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "26.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ada3449bb23c964a88a1bf633ac66ca3ebac2061693f53148b199ce816791d0" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231ae1585d14b5059adde392ce80f6b151e0264ed62b121bca9fe025e8ace460" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de6ad39b7070e8703d01c3abad13e75ea5e65b1a5ce5e86b98d724773282044" +dependencies = [ + "arbitrary", + "bytes-lit", + "crate-git-revision", + "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 0.0.16", + "visibility", +] + +[[package]] +name = "soroban-sdk-macros" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cd5e180920e224d209562cccffd27c35884d14c4dc296b295dfcebef46b6f1" +dependencies = [ + "darling 0.20.11", + "heck", + "itertools", + "macro-string", + "proc-macro2", + "quote", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-spec" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680c70a2ab6d7cde343e9a51dd141f2f4304a7282452df8dc540b9bf62565886" +dependencies = [ + "base64", + "sha2", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e368f91c6633c25cd2d143fd82f84206b8e58718fc630d344f2fa33e6d31b1b" +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 = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" +dependencies = [ + "crate-git-revision", + "data-encoding", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084afcb0d458c3d5d5baa2d294b18f881e62cc258ef539d8fdf68be7dbe45520" +dependencies = [ + "crate-git-revision", + "data-encoding", + "heapless", +] + +[[package]] +name = "stellar-xdr" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6e29c7e1f071c2767916460d006668197843d5d93f0ec8893a26f72a14f595" +dependencies = [ + "arbitrary", + "base64", + "cfg_eval", + "crate-git-revision", + "escape-bytes", + "ethnum", + "hex", + "serde", + "serde_with", + "sha2", + "stellar-strkey 0.0.13", +] + +[[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 = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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 = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "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.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/stellar/Cargo.toml b/stellar/Cargo.toml new file mode 100644 index 0000000..a461ebb --- /dev/null +++ b/stellar/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "donation-handler" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT" +description = "Soroban port of the Giveth DonationHandler contract" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = "26.0.1" + +[dev-dependencies] +soroban-sdk = { version = "26.0.1", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true diff --git a/stellar/README.md b/stellar/README.md new file mode 100644 index 0000000..1709eaa --- /dev/null +++ b/stellar/README.md @@ -0,0 +1,120 @@ +# DonationHandler — Soroban (Stellar) + +A Soroban (Rust) port of the EVM [`DonationHandler.sol`](../src/contracts/DonationHandler.sol). +It facilitates single and batched donations of **native XLM and any Stellar-issued +asset** (USDC, etc.) through one unified entrypoint, and emits a `DonationMade` +event carrying an arbitrary `data` payload (used by the Giveth indexer to attach a +`projectId`). + +See [`../stellar-feasibility-report.md`](../stellar-feasibility-report.md) (in the +`giveth-v6-core` repo) for the full architecture rationale. + +## Why this differs from the EVM contract + +| EVM (`DonationHandler.sol`) | Soroban (`donation-handler`) | +| ------------------------------------------------------- | ----------------------------------------------------- | +| `donateETH` / `donateManyETH` | `donate` / `donate_many` with `token` = native XLM SAC | +| `donateERC20` / `donateManyERC20` | `donate` / `donate_many` with `token` = issued-asset SAC | +| Native vs. ERC20 split (`msg.value` vs. `transferFrom`) | Unified via the **Stellar Asset Contract (SAC)** — native XLM and issued assets share the SEP-41 token interface | +| `ReentrancyGuard` | **Not needed** — see below | +| OZ upgradeable proxy (`initialize` + proxy) | `update_current_contract_wasm`, admin-gated `upgrade` | +| `DonationMade(recipient, amount, token, data)` event | `DonationMade` event, same fields (see schema below) | + +### No ReentrancyGuard — rationale + +Soroban's authorization framework requires a signed, deterministic auth tree for +every `require_auth` and SAC transfer, verified by the host before execution. +This contract additionally: + +1. **Never calls user-controlled contracts** — only the host-provided SAC, which + cannot re-enter our code. +2. **Holds no mutable balance state** — funds move directly donor → recipient; + the only persistent value is the `admin` (for upgrade auth). + +If the contract is ever extended to call arbitrary recipient hooks, revisit this +and add a manual guard. + +## Public interface + +```rust +fn initialize(admin: Address); // once; sets the admin (Ownable) +fn donate(from, token, recipient, amount, data); // single donation +fn donate_many(from, token, total_amount, // batch; sum(amounts) must == total_amount + recipients, amounts, data); +fn admin() -> Address; +fn set_admin(new_admin: Address); // admin-gated +fn upgrade(new_wasm_hash: BytesN<32>); // admin-gated WASM upgrade +``` + +Validation mirrors the EVM contract: array lengths must match, batches must be +non-empty, every amount must be `> 0`, and `sum(amounts)` must equal the declared +`total_amount` (else `AmountsMismatch`). + +## `DonationMade` event schema (for the indexer) + +* **topics**: `[ Symbol("donation"), recipient: Address, token: Address ]` +* **data** (map): `{ amount: i128, data: Bytes }` + +The indexer subscribes via Soroban RPC `getEvents` filtered on +`(contractId, topic[0] == "donation")`, then reads the `projectId` out of +`data.data`. This maps 1:1 to the EVM `DonationMade(recipient indexed, amount, +token indexed, data)` (indexed fields → topics, non-indexed → data). + +## Build & test + +```bash +# Prerequisites: rustup, the wasm target, and the stellar CLI +rustup target add wasm32v1-none +brew install stellar-cli # or: cargo install --locked stellar-cli + +cargo test # 10 unit tests (testutils) +stellar contract build # -> target/wasm32v1-none/release/donation_handler.wasm +``` + +## Deploy (testnet) + +```bash +stellar keys generate deployer --network testnet --fund +DEPLOYER=$(stellar keys address deployer) + +stellar contract deploy \ + --wasm target/wasm32v1-none/release/donation_handler.wasm \ + --source deployer --network testnet --alias donation_handler + +stellar contract invoke --id donation_handler --source deployer --network testnet \ + -- initialize --admin "$DEPLOYER" +``` + +Single donation of 1 XLM (native SAC), with a `projectId` payload: + +```bash +NATIVE=$(stellar contract id asset --asset native --network testnet) +stellar contract invoke --id donation_handler --source deployer --network testnet --send=yes \ + -- donate --from "$DEPLOYER" --token "$NATIVE" \ + --recipient --amount 10000000 --data 0000002a +``` + +Batch donation (`Vec` args as JSON): + +```bash +stellar contract invoke --id donation_handler --source deployer --network testnet --send=yes \ + -- donate_many --from "$DEPLOYER" --token "$NATIVE" --total-amount 30000000 \ + --recipients '["G...","G..."]' --amounts '["10000000","20000000"]' \ + --data '["0000002a","0000002b"]' +``` + +## Current testnet deployment + +| Field | Value | +| ---------------- | ------------------------------------------------------------ | +| Network | Test SDF Network ; September 2015 | +| Contract ID | `CDBJL3AEFCA2ECOBUJ4G622Y63XGKX3UXBVVODM4PORVE3DT3PJ4VE4S` | +| WASM hash | `f4652257bec0efb6f09f3ff5470c0b792dd29611f51b9b33c592fa044c87ea9a` | +| Admin | `GDKJHXJGKQXPRUWZCMSZM2QJAERFCMQJGLBREYLTVP4MYYBTQKM2DE7X` | +| Explorer | https://stellar.expert/explorer/testnet/contract/CDBJL3AEFCA2ECOBUJ4G622Y63XGKX3UXBVVODM4PORVE3DT3PJ4VE4S | + +Verified on-chain: a single donation and a 2-recipient batch both transferred +funds and emitted `DonationMade` with the `data` (projectId) payload intact. + +> **Note:** This contract has not been audited. Do not deploy to mainnet without a +> security review. diff --git a/stellar/src/lib.rs b/stellar/src/lib.rs new file mode 100644 index 0000000..c1d5ebe --- /dev/null +++ b/stellar/src/lib.rs @@ -0,0 +1,208 @@ +#![no_std] +//! # DonationHandler (Soroban) +//! +//! Soroban port of the EVM `DonationHandler.sol` contract. It facilitates +//! single and batched donations and emits a `DonationMade` event carrying an +//! arbitrary `data` payload (used by the Giveth indexer to attach a +//! `projectId`). +//! +//! ## Differences vs. the EVM contract +//! +//! * The EVM contract has four entrypoints (`donateETH`, `donateManyETH`, +//! `donateERC20`, `donateManyERC20`). On Soroban the native-vs-token split +//! disappears: the [Stellar Asset Contract (SAC)] exposes native XLM and +//! every issued asset (USDC, etc.) behind the same SEP-41 token interface, so +//! a single `token: Address` argument covers both. The four functions +//! collapse into [`donate`] and [`donate_many`]. +//! * Funds move directly from the donor to the recipient via +//! `token.transfer(from, recipient, amount)` (mirroring the EVM +//! `safeTransferFrom`). The contract never custodies funds. +//! * No `ReentrancyGuard`. Soroban's authorization framework requires a signed, +//! deterministic auth tree for every `require_auth`/SAC transfer, and this +//! contract only ever calls the host-provided SAC (which cannot re-enter +//! user code) and holds no mutable balance state. See the project README for +//! the full rationale. +//! * Upgradeability uses `update_current_contract_wasm` gated by the admin +//! (the Soroban equivalent of the OpenZeppelin upgradeable proxy pattern). +//! +//! [Stellar Asset Contract (SAC)]: https://developers.stellar.org/docs/tokens/stellar-asset-contract + +use soroban_sdk::{ + contract, contractevent, contracterror, contractimpl, symbol_short, token, Address, Bytes, + BytesN, Env, Symbol, Vec, +}; + +/// Instance-storage key holding the admin address (mirrors `Ownable`). +const ADMIN: Symbol = symbol_short!("ADMIN"); + +/// Emitted for every donation. Mirrors the EVM +/// `DonationMade(address indexed recipientAddress, uint256 amount, address indexed tokenAddress, bytes data)`. +/// +/// Indexed fields become topics; the topic prefix `"donation"` (topic[0]) lets +/// the indexer subscribe with a single server-side filter on +/// `(contract_id, topic[0] == "donation")`. The `data` map carries the opaque +/// `data` payload (the Giveth `projectId`) and the `amount`. +#[contractevent(topics = ["donation"], data_format = "map")] +pub struct DonationMade { + #[topic] + pub recipient: Address, + #[topic] + pub token: Address, + pub amount: i128, + pub data: Bytes, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + /// Contract has not been initialized with an admin yet. + NotInitialized = 1, + /// `initialize` was called more than once. + AlreadyInitialized = 2, + /// Array lengths mismatch, an empty batch, or a non-positive amount. + /// (Equivalent to the EVM `InvalidInput` error.) + InvalidInput = 3, + /// `sum(amounts) != total_amount` (EVM: "Amounts do not match total"). + AmountsMismatch = 4, + /// Arithmetic overflow while summing batch amounts. + Overflow = 5, +} + +#[contract] +pub struct DonationHandler; + +#[contractimpl] +impl DonationHandler { + /// Initialize the contract with an `admin`. Mirrors the EVM + /// `initialize()` + `__Ownable_init`. Can only be called once. + pub fn initialize(env: Env, admin: Address) -> Result<(), Error> { + if env.storage().instance().has(&ADMIN) { + return Err(Error::AlreadyInitialized); + } + env.storage().instance().set(&ADMIN, &admin); + Ok(()) + } + + /// Single donation. + /// + /// * `from` – the donor; must authorize the call. + /// * `token` – SAC address of the asset (native XLM SAC or an issued + /// asset such as USDC). + /// * `recipient` – the project/recipient address. + /// * `amount` – amount in the token's smallest unit (must be > 0). + /// * `data` – opaque payload (the Giveth `projectId`, etc.). + /// + /// Equivalent to `donateETH` / `donateERC20`. + pub fn donate( + env: Env, + from: Address, + token: Address, + recipient: Address, + amount: i128, + data: Bytes, + ) -> Result<(), Error> { + from.require_auth(); + Self::handle_one(&env, &from, &token, &recipient, amount, &data) + } + + /// Batch donation to multiple recipients in a single transaction. + /// + /// `recipients`, `amounts` and `data` must all have the same (non-zero) + /// length, and `sum(amounts)` must equal `total_amount`. Equivalent to + /// `donateManyETH` / `donateManyERC20`. + pub fn donate_many( + env: Env, + from: Address, + token: Address, + total_amount: i128, + recipients: Vec
, + amounts: Vec, + data: Vec, + ) -> Result<(), Error> { + from.require_auth(); + + let len = recipients.len(); + if len == 0 || len != amounts.len() || len != data.len() { + return Err(Error::InvalidInput); + } + + // Validate amounts and confirm the declared total (EVM: sum == total). + let mut sum: i128 = 0; + for amount in amounts.iter() { + if amount <= 0 { + return Err(Error::InvalidInput); + } + sum = sum.checked_add(amount).ok_or(Error::Overflow)?; + } + if sum != total_amount { + return Err(Error::AmountsMismatch); + } + + for i in 0..len { + // Safe: index < len, and the three vecs share the same length. + let recipient = recipients.get_unchecked(i); + let amount = amounts.get_unchecked(i); + let datum = data.get_unchecked(i); + Self::handle_one(&env, &from, &token, &recipient, amount, &datum)?; + } + Ok(()) + } + + /// Return the current admin. Errors if the contract is uninitialized. + pub fn admin(env: Env) -> Result { + env.storage() + .instance() + .get(&ADMIN) + .ok_or(Error::NotInitialized) + } + + /// Transfer ownership to a new admin. Admin-gated. + pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> { + let admin = Self::admin(env.clone())?; + admin.require_auth(); + env.storage().instance().set(&ADMIN, &new_admin); + Ok(()) + } + + /// Upgrade the contract's WASM bytecode. Admin-gated. This is the Soroban + /// equivalent of the OpenZeppelin upgradeable proxy `upgradeTo`. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), Error> { + let admin = Self::admin(env.clone())?; + admin.require_auth(); + env.deployer().update_current_contract_wasm(new_wasm_hash); + Ok(()) + } +} + +impl DonationHandler { + /// Execute one donation: validate, transfer donor -> recipient, emit event. + fn handle_one( + env: &Env, + from: &Address, + token: &Address, + recipient: &Address, + amount: i128, + data: &Bytes, + ) -> Result<(), Error> { + if amount <= 0 { + return Err(Error::InvalidInput); + } + + // The SAC unifies native XLM and issued assets behind SEP-41. The + // transfer itself requires `from`'s authorization (verified by the + // host), so funds can never move without the donor's signed consent. + token::TokenClient::new(env, token).transfer(from, recipient, &amount); + + env.events().publish_event(&DonationMade { + recipient: recipient.clone(), + token: token.clone(), + amount, + data: data.clone(), + }); + Ok(()) + } +} + +#[cfg(test)] +mod test; diff --git a/stellar/src/test.rs b/stellar/src/test.rs new file mode 100644 index 0000000..ab6f1f8 --- /dev/null +++ b/stellar/src/test.rs @@ -0,0 +1,201 @@ +#![cfg(test)] +extern crate std; + +use super::{DonationHandler, DonationHandlerClient, Error}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + token, vec, Address, Bytes, Env, +}; + +struct Setup<'a> { + env: Env, + contract_id: Address, + client: DonationHandlerClient<'a>, + admin: Address, + donor: Address, + token: Address, + token_client: token::TokenClient<'a>, +} + +fn setup() -> Setup<'static> { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let donor = Address::generate(&env); + + // Register the SAC for a test asset and mint a balance to the donor. + let issuer = Address::generate(&env); + let sac = env.register_stellar_asset_contract_v2(issuer); + let token = sac.address(); + let token_sac = token::StellarAssetClient::new(&env, &token); + let token_client = token::TokenClient::new(&env, &token); + token_sac.mint(&donor, &1_000_000); + + let contract_id = env.register(DonationHandler, ()); + let client = DonationHandlerClient::new(&env, &contract_id); + client.initialize(&admin); + + Setup { + env, + contract_id, + client, + admin, + donor, + token, + token_client, + } +} + +/// Count of events emitted by our contract in the last invocation. Our +/// contract only ever emits `DonationMade`, so this is the donation count +/// (SAC `transfer` events are attributed to the token contract and excluded). +fn donation_events(s: &Setup) -> usize { + s.env + .events() + .all() + .filter_by_contract(&s.contract_id) + .events() + .len() +} + +#[test] +fn initialize_sets_admin() { + let s = setup(); + assert_eq!(s.client.admin(), s.admin); +} + +#[test] +fn initialize_twice_fails() { + let s = setup(); + let other = Address::generate(&s.env); + assert_eq!(s.client.try_initialize(&other), Err(Ok(Error::AlreadyInitialized))); +} + +#[test] +fn donate_single_transfers_and_emits() { + let s = setup(); + let recipient = Address::generate(&s.env); + let data = Bytes::from_array(&s.env, &[1, 2, 3, 4]); // pretend projectId + + s.client.donate(&s.donor, &s.token, &recipient, &500, &data); + + // `events().all()` reflects only the most recent contract invocation, so + // assert the event count before issuing any `balance` queries (which are + // themselves contract invocations). + assert_eq!(donation_events(&s), 1); + assert_eq!(s.token_client.balance(&recipient), 500); + assert_eq!(s.token_client.balance(&s.donor), 1_000_000 - 500); +} + +#[test] +fn donate_zero_amount_fails() { + let s = setup(); + let recipient = Address::generate(&s.env); + let data = Bytes::new(&s.env); + assert_eq!( + s.client.try_donate(&s.donor, &s.token, &recipient, &0, &data), + Err(Ok(Error::InvalidInput)) + ); +} + +#[test] +fn donate_many_distributes_to_all() { + let s = setup(); + let r1 = Address::generate(&s.env); + let r2 = Address::generate(&s.env); + let r3 = Address::generate(&s.env); + + let recipients = vec![&s.env, r1.clone(), r2.clone(), r3.clone()]; + let amounts = vec![&s.env, 100i128, 250i128, 150i128]; + let data = vec![ + &s.env, + Bytes::from_array(&s.env, &[1]), + Bytes::from_array(&s.env, &[2]), + Bytes::from_array(&s.env, &[3]), + ]; + + s.client + .donate_many(&s.donor, &s.token, &500, &recipients, &amounts, &data); + + // Assert event count before balance queries (see note above). + assert_eq!(donation_events(&s), 3); + assert_eq!(s.token_client.balance(&r1), 100); + assert_eq!(s.token_client.balance(&r2), 250); + assert_eq!(s.token_client.balance(&r3), 150); + assert_eq!(s.token_client.balance(&s.donor), 1_000_000 - 500); +} + +#[test] +fn donate_many_length_mismatch_fails() { + let s = setup(); + let r1 = Address::generate(&s.env); + let recipients = vec![&s.env, r1.clone()]; + let amounts = vec![&s.env, 100i128, 200i128]; // mismatched length + let data = vec![&s.env, Bytes::from_array(&s.env, &[1])]; + + assert_eq!( + s.client + .try_donate_many(&s.donor, &s.token, &300, &recipients, &amounts, &data), + Err(Ok(Error::InvalidInput)) + ); + // No funds moved. + assert_eq!(s.token_client.balance(&s.donor), 1_000_000); +} + +#[test] +fn donate_many_sum_mismatch_fails() { + let s = setup(); + let r1 = Address::generate(&s.env); + let r2 = Address::generate(&s.env); + let recipients = vec![&s.env, r1.clone(), r2.clone()]; + let amounts = vec![&s.env, 100i128, 200i128]; // sums to 300 + let data = vec![ + &s.env, + Bytes::from_array(&s.env, &[1]), + Bytes::from_array(&s.env, &[2]), + ]; + + // Declared total (999) != sum (300). + assert_eq!( + s.client + .try_donate_many(&s.donor, &s.token, &999, &recipients, &amounts, &data), + Err(Ok(Error::AmountsMismatch)) + ); + assert_eq!(s.token_client.balance(&s.donor), 1_000_000); +} + +#[test] +fn donate_many_empty_fails() { + let s = setup(); + let recipients = soroban_sdk::Vec::
::new(&s.env); + let amounts = soroban_sdk::Vec::::new(&s.env); + let data = soroban_sdk::Vec::::new(&s.env); + assert_eq!( + s.client + .try_donate_many(&s.donor, &s.token, &0, &recipients, &amounts, &data), + Err(Ok(Error::InvalidInput)) + ); +} + +#[test] +fn set_admin_changes_owner() { + let s = setup(); + let new_admin = Address::generate(&s.env); + s.client.set_admin(&new_admin); + assert_eq!(s.client.admin(), new_admin); +} + +#[test] +fn donor_balance_insufficient_panics() { + // The SAC transfer itself enforces balance; donating more than the donor + // holds must fail (the whole tx reverts). + let s = setup(); + let recipient = Address::generate(&s.env); + let data = Bytes::new(&s.env); + let res = s + .client + .try_donate(&s.donor, &s.token, &recipient, &2_000_000, &data); + assert!(res.is_err()); + assert_eq!(s.token_client.balance(&recipient), 0); +} From 71c1b6986fc9915902131fb82140b93b22dbf20e Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 29 May 2026 23:07:37 +0330 Subject: [PATCH 2/2] refactor: set admin via constructor and harden batch loop Review fixes for the Soroban DonationHandler: - Replace the two-step initialize() with __constructor(admin), which runs atomically at deploy. This removes the front-running window where an attacker could call initialize() before the deployer and seize admin (and thus upgrade) rights. - Iterate the batch vecs with zip() instead of get_unchecked indexing. - Add a test proving a batch with insufficient funds reverts atomically (no partial donations); drop the now-obsolete initialize-twice test. - Remove a duplicated cfg(test) attribute; clippy is clean. Redeployed to testnet: CCAWXIU37ILOKXRPJVL56VJAVLJRKGNFSIXCAOLO3YFEDJV6DZRMJLAL (admin set by constructor, donation verified on-chain). Co-Authored-By: Claude Opus 4.7 (1M context) --- stellar/README.md | 21 +++++++++++---------- stellar/src/lib.rs | 37 +++++++++++++++++++------------------ stellar/src/test.rs | 40 +++++++++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 39 deletions(-) diff --git a/stellar/README.md b/stellar/README.md index 1709eaa..34dbe41 100644 --- a/stellar/README.md +++ b/stellar/README.md @@ -37,7 +37,7 @@ and add a manual guard. ## Public interface ```rust -fn initialize(admin: Address); // once; sets the admin (Ownable) +fn __constructor(admin: Address); // runs at deploy; sets the admin (Ownable) fn donate(from, token, recipient, amount, data); // single donation fn donate_many(from, token, total_amount, // batch; sum(amounts) must == total_amount recipients, amounts, data); @@ -77,12 +77,12 @@ stellar contract build # -> target/wasm32v1-none/release/donation_h stellar keys generate deployer --network testnet --fund DEPLOYER=$(stellar keys address deployer) +# The admin is set atomically by the constructor (the `-- --admin` args), +# so there is no separate, front-runnable initialize step. stellar contract deploy \ --wasm target/wasm32v1-none/release/donation_handler.wasm \ - --source deployer --network testnet --alias donation_handler - -stellar contract invoke --id donation_handler --source deployer --network testnet \ - -- initialize --admin "$DEPLOYER" + --source deployer --network testnet --alias donation_handler \ + -- --admin "$DEPLOYER" ``` Single donation of 1 XLM (native SAC), with a `projectId` payload: @@ -108,13 +108,14 @@ stellar contract invoke --id donation_handler --source deployer --network testne | Field | Value | | ---------------- | ------------------------------------------------------------ | | Network | Test SDF Network ; September 2015 | -| Contract ID | `CDBJL3AEFCA2ECOBUJ4G622Y63XGKX3UXBVVODM4PORVE3DT3PJ4VE4S` | -| WASM hash | `f4652257bec0efb6f09f3ff5470c0b792dd29611f51b9b33c592fa044c87ea9a` | +| Contract ID | `CCAWXIU37ILOKXRPJVL56VJAVLJRKGNFSIXCAOLO3YFEDJV6DZRMJLAL` | +| WASM hash | `a6509e7036715940040136f651e5a3e9520c02e47fbba7c3226c4a885717c4d3` | | Admin | `GDKJHXJGKQXPRUWZCMSZM2QJAERFCMQJGLBREYLTVP4MYYBTQKM2DE7X` | -| Explorer | https://stellar.expert/explorer/testnet/contract/CDBJL3AEFCA2ECOBUJ4G622Y63XGKX3UXBVVODM4PORVE3DT3PJ4VE4S | +| Explorer | https://stellar.expert/explorer/testnet/contract/CCAWXIU37ILOKXRPJVL56VJAVLJRKGNFSIXCAOLO3YFEDJV6DZRMJLAL | -Verified on-chain: a single donation and a 2-recipient batch both transferred -funds and emitted `DonationMade` with the `data` (projectId) payload intact. +Verified on testnet: the admin is set by the constructor at deploy, and single +and batch donations both transfer funds and emit `DonationMade` with the `data` +(projectId) payload intact. > **Note:** This contract has not been audited. Do not deploy to mainnet without a > security review. diff --git a/stellar/src/lib.rs b/stellar/src/lib.rs index c1d5ebe..47c748a 100644 --- a/stellar/src/lib.rs +++ b/stellar/src/lib.rs @@ -56,17 +56,17 @@ pub struct DonationMade { #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { - /// Contract has not been initialized with an admin yet. + /// Admin not set — instance storage missing or archived. With the + /// constructor this is effectively unreachable, but it is kept as a + /// defensive guard for the admin-gated functions. NotInitialized = 1, - /// `initialize` was called more than once. - AlreadyInitialized = 2, /// Array lengths mismatch, an empty batch, or a non-positive amount. /// (Equivalent to the EVM `InvalidInput` error.) - InvalidInput = 3, + InvalidInput = 2, /// `sum(amounts) != total_amount` (EVM: "Amounts do not match total"). - AmountsMismatch = 4, + AmountsMismatch = 3, /// Arithmetic overflow while summing batch amounts. - Overflow = 5, + Overflow = 4, } #[contract] @@ -74,14 +74,13 @@ pub struct DonationHandler; #[contractimpl] impl DonationHandler { - /// Initialize the contract with an `admin`. Mirrors the EVM - /// `initialize()` + `__Ownable_init`. Can only be called once. - pub fn initialize(env: Env, admin: Address) -> Result<(), Error> { - if env.storage().instance().has(&ADMIN) { - return Err(Error::AlreadyInitialized); - } + /// Constructor — runs atomically as part of contract deployment. Sets the + /// `admin` (the Ownable-equivalent owner). Running this at deploy time, + /// rather than via a separate `initialize` call, removes the front-running + /// window a two-step init would expose: otherwise an attacker could claim + /// `admin` — and therefore `upgrade` rights — before the deployer. + pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&ADMIN, &admin); - Ok(()) } /// Single donation. @@ -139,11 +138,13 @@ impl DonationHandler { return Err(Error::AmountsMismatch); } - for i in 0..len { - // Safe: index < len, and the three vecs share the same length. - let recipient = recipients.get_unchecked(i); - let amount = amounts.get_unchecked(i); - let datum = data.get_unchecked(i); + // Execute every transfer, iterating the three equal-length vecs in + // lock-step (no index arithmetic). A failure in any transfer (e.g. + // insufficient balance) panics and reverts the whole transaction + // atomically — there are no partial donations. + for ((recipient, amount), datum) in + recipients.iter().zip(amounts.iter()).zip(data.iter()) + { Self::handle_one(&env, &from, &token, &recipient, amount, &datum)?; } Ok(()) diff --git a/stellar/src/test.rs b/stellar/src/test.rs index ab6f1f8..572d973 100644 --- a/stellar/src/test.rs +++ b/stellar/src/test.rs @@ -1,4 +1,3 @@ -#![cfg(test)] extern crate std; use super::{DonationHandler, DonationHandlerClient, Error}; @@ -32,9 +31,9 @@ fn setup() -> Setup<'static> { let token_client = token::TokenClient::new(&env, &token); token_sac.mint(&donor, &1_000_000); - let contract_id = env.register(DonationHandler, ()); + // Admin is set atomically by the constructor at registration/deploy time. + let contract_id = env.register(DonationHandler, (admin.clone(),)); let client = DonationHandlerClient::new(&env, &contract_id); - client.initialize(&admin); Setup { env, @@ -60,18 +59,11 @@ fn donation_events(s: &Setup) -> usize { } #[test] -fn initialize_sets_admin() { +fn constructor_sets_admin() { let s = setup(); assert_eq!(s.client.admin(), s.admin); } -#[test] -fn initialize_twice_fails() { - let s = setup(); - let other = Address::generate(&s.env); - assert_eq!(s.client.try_initialize(&other), Err(Ok(Error::AlreadyInitialized))); -} - #[test] fn donate_single_transfers_and_emits() { let s = setup(); @@ -178,6 +170,32 @@ fn donate_many_empty_fails() { ); } +#[test] +fn donate_many_reverts_atomically_on_insufficient_funds() { + // Donor holds 1_000_000. A batch totalling 1_200_000 must move *nothing*: + // the second transfer fails and the whole transaction rolls back. + let s = setup(); + let r1 = Address::generate(&s.env); + let r2 = Address::generate(&s.env); + let recipients = vec![&s.env, r1.clone(), r2.clone()]; + let amounts = vec![&s.env, 600_000i128, 600_000i128]; + let data = vec![ + &s.env, + Bytes::from_array(&s.env, &[1]), + Bytes::from_array(&s.env, &[2]), + ]; + + let res = s + .client + .try_donate_many(&s.donor, &s.token, &1_200_000, &recipients, &amounts, &data); + assert!(res.is_err()); + + // No partial donation: the first recipient must not have been paid. + assert_eq!(s.token_client.balance(&r1), 0); + assert_eq!(s.token_client.balance(&r2), 0); + assert_eq!(s.token_client.balance(&s.donor), 1_000_000); +} + #[test] fn set_admin_changes_owner() { let s = setup();