From 60678c6aadc11c24409e00ced1fe9ec3df1788f4 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:37:21 +0000 Subject: [PATCH 01/15] Add modular_account example for CAP-71 auth delegation Adds a new modular_account example demonstrating the auth delegation APIs introduced in soroban-sdk v27 via CAP-71: - env.custom_account().get_delegated_signers() to read the delegate addresses the user attached to the auth entry. - env.custom_account().delegate_auth(&address) to forward the current __check_auth context to a verified delegate. ModularAccount performs its own ed25519 verification, then forwards the auth context to each registered delegate. The test exercises the full flow via env.set_auths with SorobanAddressCredentialsWithDelegates, covering both an unregistered-delegate rejection and a successful delegation to two registered signers. Claude-Session: https://claude.ai/code/session_01EgjfKqQ1RMvtwQUSx3iDzh --- README.md | 1 + modular_account/Cargo.lock | 1919 +++++++++++++++++++++++++++++++++++ modular_account/Cargo.toml | 32 + modular_account/Makefile | 16 + modular_account/src/lib.rs | 118 +++ modular_account/src/test.rs | 234 +++++ 6 files changed, 2320 insertions(+) create mode 100644 modular_account/Cargo.lock create mode 100644 modular_account/Cargo.toml create mode 100644 modular_account/Makefile create mode 100644 modular_account/src/lib.rs create mode 100644 modular_account/src/test.rs diff --git a/README.md b/README.md index 2814ac74..d3892c55 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ atomic_multiswap**: This contract performs a batch of atomic token swaps between - **logging**: A basic example of how to use the standard Soroban terminal logging - **merkle_distribution**: A Merkle distribution contract that verifies Merkle proofs to distribute tokens efficiently to eligible recipients - **mint-lock**: Demonstrates token minting, including minting authorization +- **modular_account**: A custom account contract that uses CAP-71 auth delegation to forward `__check_auth` to registered delegate signers - **other_custom_types**: The smart contract implements types, including custom types - **privacy-pools**: A prototype for Privacy Pools for Soroban. - **simple_account**: A minimal example of an account contract, owned by a single ed25519 public key diff --git a/modular_account/Cargo.lock b/modular_account/Cargo.lock new file mode 100644 index 00000000..29029e98 --- /dev/null +++ b/modular_account/Cargo.lock @@ -0,0 +1,1919 @@ +# 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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b04f2b1d34cb428043f14aa4c853d14294532e8bbde3b6a3bc2faaaae31a1dd" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +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 = "crate-git-revision" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54851b5b3f24621804b1cded2820975623c205e3055d2d44031cdb1237339ac8" +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 = [ + "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 = "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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[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.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +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.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d769030e3b2c27873e9be4931decbbe79787b943d5b30e31f8c395eae83144b8" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa50d998c0baafcc6078cb94f5228040c7719b1608c15debc3fc78365dc22f5" +dependencies = [ + "arbitrary", + "crate-git-revision 0.0.6", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde1064f5e92dbcc8018ecc8e92728bf5bbff0236abaf792c6858367a6853415" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47762a1b75b9ccd07558f28e1a0289ca6adec56810c581cde5cf16e329e00b9" +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 = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04b813a9e32456b37875fc41cdb05912a9e947000b9414b7985bffe07a889a" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "27.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2272e9ee6f44cae1f3fe38b16f83e37ae6b55d002ecd7cbd45e02fde277c5a0" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-modular-account-contract" +version = "0.0.0" +dependencies = [ + "ed25519-dalek", + "sha2", + "soroban-sdk", +] + +[[package]] +name = "soroban-sdk" +version = "27.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de4cf66a6ec501ca3bb6018e36d3293ffbcd9b83651d8ae5afa03a4c8dd02fd1" +dependencies = [ + "arbitrary", + "bytes-lit", + "crate-git-revision 0.0.9", + "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 = "27.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ecdb16404a2a973c88193ccbfe519aa44b02cd2ff838dd61ce49117a2537c" +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 = "27.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85665c21947b7e9fff81a8e81c866e2e774040039856f75af9cb06ec75191c8" +dependencies = [ + "base64", + "sha2", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "27.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb924883f555ca020951e861025d153657481e0a88dddec9f853e2aad76ddc9" +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 0.0.6", + "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 0.0.6", + "data-encoding", + "heapless", +] + +[[package]] +name = "stellar-xdr" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ff843326969bdf1ef673dcdba94c08f4a3c8f1e58d6e6ef39b1bd4f749179a" +dependencies = [ + "arbitrary", + "base64", + "cfg_eval", + "crate-git-revision 0.0.6", + "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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +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.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +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.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +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.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +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/modular_account/Cargo.toml b/modular_account/Cargo.toml new file mode 100644 index 00000000..1f5600b8 --- /dev/null +++ b/modular_account/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "soroban-modular-account-contract" +version = "0.0.0" +edition = "2021" +publish = false +rust-version = "1.89.0" + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { version = "27.0.0-rc.1" } + +[dev-dependencies] +soroban-sdk = { version = "27.0.0-rc.1", features = ["testutils"] } +ed25519-dalek = { version = "2.1.1" } +sha2 = { version = "0.10.8" } + +[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/modular_account/Makefile b/modular_account/Makefile new file mode 100644 index 00000000..b9719346 --- /dev/null +++ b/modular_account/Makefile @@ -0,0 +1,16 @@ +default: build + +all: test + +test: build + cargo test + +build: + stellar contract build + @ls -l target/wasm32v1-none/release/*.wasm + +fmt: + cargo fmt --all + +clean: + cargo clean diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs new file mode 100644 index 00000000..238159f1 --- /dev/null +++ b/modular_account/src/lib.rs @@ -0,0 +1,118 @@ +//! This is a modular account contract that delegates (part of) its +//! authentication to a set of registered signer addresses. +//! +//! Instead of performing all of the signature verification itself, the +//! `ModularAccount` contract forwards its `__check_auth` context to one or more +//! registered delegate signers. Each delegate then runs its own `__check_auth` +//! independently. The user chooses which of the registered signers to +//! authenticate with by attaching them to the transaction's authorization +//! payload as delegated signers. +//! +//! Auth delegation was introduced in soroban-sdk v27 via CAP-71: +//! . +//! +//! For a single-key account see the `simple_account` example, and for a +//! multi-sig account with custom authorization policies see the `account` +//! example. +#![no_std] + +use soroban_sdk::{ + auth::{Context, CustomAccountInterface}, + contract, contracterror, contractimpl, contracttype, + crypto::Hash, + Address, BytesN, Env, Symbol, Vec, +}; + +#[contracterror] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ModularAccountError { + UnknownDelegate = 1, +} + +#[contracttype] +pub enum DataKey { + // The account's own ed25519 public key. + PublicKey, + // The set of addresses allowed to act as delegates for this account. + Signers, + // A log of the function names this account has approved, used by the tests + // to verify what was authorized. + AuthorizedCalls, +} + +// A custom account that can delegate authentication to a set of registered +// signer addresses. +#[contract] +pub struct ModularAccount; + +#[contractimpl] +impl ModularAccount { + pub fn __constructor(env: Env, public_key: BytesN<32>, signers: Vec
) { + env.storage() + .instance() + .set(&DataKey::PublicKey, &public_key); + env.storage().instance().set(&DataKey::Signers, &signers); + } +} + +#[contractimpl] +impl CustomAccountInterface for ModularAccount { + type Signature = BytesN<64>; + type Error = ModularAccountError; + + fn __check_auth( + env: Env, + signature_payload: Hash<32>, + signature: BytesN<64>, + auth_contexts: Vec, + ) -> Result<(), ModularAccountError> { + // Even though we use delegated authentication, the account can still + // perform the regular verification if necessary. + let public_key: BytesN<32> = env.storage().instance().get(&DataKey::PublicKey).unwrap(); + env.crypto() + .ed25519_verify(&public_key, &signature_payload.into(), &signature); + record_authorized_calls(&env, &auth_contexts); + + // The signers the user attached to the auth entry for this account's + // authorization. These are unsanitized user input, so the account must + // verify each one against its own registered signers below. + let delegates = env.custom_account().get_delegated_signers(); + + let signers: Vec
= env.storage().instance().get(&DataKey::Signers).unwrap(); + for delegate in delegates.iter() { + // The host can not validate the delegates, so the account has to + // check that each one is actually a registered signer. + if !signers.contains(&delegate) { + return Err(ModularAccountError::UnknownDelegate); + } + // Forward the current authentication context to the delegate. Unlike + // `require_auth`, this does not start a new contract invocation and + // does not require a separate auth entry for the delegate in the + // transaction. Delegation is nestable: a delegate may further + // delegate. + env.custom_account().delegate_auth(&delegate); + } + Ok(()) + } +} + +// Appends the function name from each contract-call context to a per-account +// log in instance storage so the tests can verify what the account approved. +fn record_authorized_calls(env: &Env, auth_contexts: &Vec) { + let mut calls: Vec = env + .storage() + .instance() + .get(&DataKey::AuthorizedCalls) + .unwrap_or_else(|| Vec::new(env)); + for ctx in auth_contexts.iter() { + if let Context::Contract(c) = ctx { + calls.push_back(c.fn_name); + } + } + env.storage() + .instance() + .set(&DataKey::AuthorizedCalls, &calls); +} + +mod test; diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs new file mode 100644 index 00000000..e3bae984 --- /dev/null +++ b/modular_account/src/test.rs @@ -0,0 +1,234 @@ +#![cfg(test)] +extern crate std; + +use ed25519_dalek::{Signer, SigningKey}; +use sha2::{Digest, Sha256}; + +use soroban_sdk::xdr::{ + HashIdPreimage, HashIdPreimageSorobanAuthorizationWithAddress, InvokeContractArgs, Limits, + ScAddress, ScVal, SorobanAddressCredentials, SorobanAddressCredentialsWithDelegates, + SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, + SorobanCredentials, SorobanDelegateSignature, StringM, VecM, WriteXdr, +}; +use soroban_sdk::{ + auth::{Context, CustomAccountInterface}, + contract, contractimpl, + crypto::Hash, + vec, Address, BytesN, Env, Symbol, TryFromVal, Vec, +}; + +use crate::{record_authorized_calls, DataKey, ModularAccount}; + +// An account that performs ed25519 verification and is used as a delegate +// signer of `ModularAccount`. Any address type (G- or C-) that implements +// `CustomAccountInterface` can be a delegate, so this is defined here as a +// test fixture rather than as a deployable contract of this crate. +#[contract] +pub struct DelegateAccount; + +#[contractimpl] +impl DelegateAccount { + pub fn __constructor(env: Env, public_key: BytesN<32>) { + env.storage() + .instance() + .set(&DataKey::PublicKey, &public_key); + } +} + +#[contractimpl] +impl CustomAccountInterface for DelegateAccount { + type Signature = BytesN<64>; + type Error = soroban_sdk::Error; + + fn __check_auth( + env: Env, + signature_payload: Hash<32>, + signature: BytesN<64>, + auth_contexts: Vec, + ) -> Result<(), soroban_sdk::Error> { + let public_key: BytesN<32> = env.storage().instance().get(&DataKey::PublicKey).unwrap(); + env.crypto() + .ed25519_verify(&public_key, &signature_payload.into(), &signature); + record_authorized_calls(&env, &auth_contexts); + Ok(()) + } +} + +// A contract with an operation that requires the account's authorization. +#[contract] +pub struct Protected; + +#[contractimpl] +impl Protected { + pub fn protected(account: Address) { + account.require_auth(); + } +} + +fn sign(env: &Env, key: &SigningKey, payload: &[u8; 32]) -> ScVal { + let sig: [u8; 64] = key.sign(payload).to_bytes(); + ScVal::try_from_val(env, &BytesN::from_array(env, &sig).to_val()).unwrap() +} + +#[test] +fn test_delegate_auth() { + let env = Env::default(); + + let account_key = SigningKey::from_bytes(&[1u8; 32]); + let key_a = SigningKey::from_bytes(&[2u8; 32]); + let key_b = SigningKey::from_bytes(&[3u8; 32]); + + let delegate_a = env.register( + DelegateAccount, + (BytesN::from_array(&env, &key_a.verifying_key().to_bytes()),), + ); + let delegate_b = env.register( + DelegateAccount, + (BytesN::from_array(&env, &key_b.verifying_key().to_bytes()),), + ); + + // Register the account with its own key and both delegates as signers. + let account = env.register( + ModularAccount, + ( + BytesN::from_array(&env, &account_key.verifying_key().to_bytes()), + vec![&env, delegate_a.clone(), delegate_b.clone()], + ), + ); + let protected = env.register(Protected, ()); + + let account_addr: ScAddress = account.clone().into(); + + let nonce = 123; + let signature_expiration_ledger = 100; + + // Create authorized invocation for the `protected` call. + let root_invocation = SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: protected.clone().into(), + function_name: StringM::try_from("protected").unwrap().into(), + args: std::vec![ScVal::Address(account_addr.clone())] + .try_into() + .unwrap(), + }), + sub_invocations: VecM::default(), + }; + + // Build the signature payload by hashing the + // `HashIdPreimage::SorobanAuthorizationWithAddress` preimage required for + // `AddressWithDelegates` credentials. + let network_id = env.ledger().network_id(); + let preimage = HashIdPreimage::SorobanAuthorizationWithAddress( + HashIdPreimageSorobanAuthorizationWithAddress { + network_id: network_id.to_array().into(), + nonce, + signature_expiration_ledger, + invocation: root_invocation.clone(), + address: account_addr.clone(), + }, + ); + let preimage_xdr = preimage.to_xdr(Limits::none()).unwrap(); + let payload: [u8; 32] = Sha256::digest(&preimage_xdr).into(); + + // Negative scenario: attach a delegate signer that isn't registered with the + // account. The account should reject the authorization with an + // `UnknownDelegate` error. + let unknown_key = SigningKey::from_bytes(&[4u8; 32]); + let unknown_delegate = env.register( + DelegateAccount, + (BytesN::from_array( + &env, + &unknown_key.verifying_key().to_bytes(), + ),), + ); + let mut bad_delegates = std::vec![ + SorobanDelegateSignature { + address: delegate_a.clone().into(), + signature: sign(&env, &key_a, &payload), + nested_delegates: VecM::default(), + }, + SorobanDelegateSignature { + address: unknown_delegate.clone().into(), + signature: sign(&env, &unknown_key, &payload), + nested_delegates: VecM::default(), + }, + ]; + // Delegates must be sorted by address. + bad_delegates.sort_by(|x, y| x.address.cmp(&y.address)); + env.set_auths(&[SorobanAuthorizationEntry { + credentials: SorobanCredentials::AddressWithDelegates( + SorobanAddressCredentialsWithDelegates { + address_credentials: SorobanAddressCredentials { + address: account_addr.clone(), + nonce, + signature_expiration_ledger, + signature: sign(&env, &account_key, &payload), + }, + delegates: bad_delegates.try_into().unwrap(), + }, + ), + root_invocation: root_invocation.clone(), + }]); + // The call will fail due to the auth failure. + assert!(ProtectedClient::new(&env, &protected) + .try_protected(&account) + .is_err()); + + // Positive scenario: each registered delegate signs the same payload with + // its own distinct key. + let mut delegates = std::vec![ + SorobanDelegateSignature { + address: delegate_a.clone().into(), + signature: sign(&env, &key_a, &payload), + nested_delegates: VecM::default(), + }, + SorobanDelegateSignature { + address: delegate_b.clone().into(), + signature: sign(&env, &key_b, &payload), + nested_delegates: VecM::default(), + }, + ]; + // Delegates must be sorted by address. + delegates.sort_by(|x, y| x.address.cmp(&y.address)); + + // Build the full authorization entry with `AddressWithDelegates` + // credentials containing both delegates. + env.set_auths(&[SorobanAuthorizationEntry { + credentials: SorobanCredentials::AddressWithDelegates( + SorobanAddressCredentialsWithDelegates { + address_credentials: SorobanAddressCredentials { + address: account_addr.clone(), + nonce, + signature_expiration_ledger, + // Also include the account's own signature in the + // credentials, as it is required by `ModularAccount`'s own + // verification step. + signature: sign(&env, &account_key, &payload), + }, + delegates: delegates.try_into().unwrap(), + }, + ), + root_invocation: root_invocation.clone(), + }]); + + // Call the `protected` function with the enforced authorization payload + // above. + // + // Note that testing delegated auth via + // `env.try_invoke_contract_check_auth` is not supported at the moment, so + // only `set_auths` plus a wrapper call can be used to test the full flow. + ProtectedClient::new(&env, &protected).protected(&account); + + // Both the account and its delegates observe a single call to `protected` + // in their authorization contexts. + let expected = vec![&env, Symbol::new(&env, "protected")]; + for addr in [&account, &delegate_a, &delegate_b] { + let calls: Vec = env.as_contract(addr, || { + env.storage() + .instance() + .get(&DataKey::AuthorizedCalls) + .unwrap() + }); + assert_eq!(calls, expected); + } +} From 9f424880bfb7d0957ded2090e50dbb997bbe05b8 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:49:29 +0000 Subject: [PATCH 02/15] Bump modular_account MSRV to 1.91.0 for soroban-sdk v27 soroban-sdk 27.0.0-rc.1 requires rustc 1.91.0, so the example's rust-version must declare 1.91.0 for the CI msrv job to select a compatible toolchain. Claude-Session: https://claude.ai/code/session_01EgjfKqQ1RMvtwQUSx3iDzh --- modular_account/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_account/Cargo.toml b/modular_account/Cargo.toml index 1f5600b8..7f0ec258 100644 --- a/modular_account/Cargo.toml +++ b/modular_account/Cargo.toml @@ -3,7 +3,7 @@ name = "soroban-modular-account-contract" version = "0.0.0" edition = "2021" publish = false -rust-version = "1.89.0" +rust-version = "1.91.0" [lib] crate-type = ["cdylib"] From dbedd3dcdcadd0371c6564c7f4aebfaa1dbd09f2 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:01:25 +0000 Subject: [PATCH 03/15] drop self-verification from modular account auth --- modular_account/src/lib.rs | 22 +++++++--------------- modular_account/src/test.rs | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 238159f1..6d0e614f 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -20,7 +20,7 @@ use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, crypto::Hash, - Address, BytesN, Env, Symbol, Vec, + Address, Env, Symbol, Vec, }; #[contracterror] @@ -32,8 +32,6 @@ pub enum ModularAccountError { #[contracttype] pub enum DataKey { - // The account's own ed25519 public key. - PublicKey, // The set of addresses allowed to act as delegates for this account. Signers, // A log of the function names this account has approved, used by the tests @@ -48,30 +46,24 @@ pub struct ModularAccount; #[contractimpl] impl ModularAccount { - pub fn __constructor(env: Env, public_key: BytesN<32>, signers: Vec
) { - env.storage() - .instance() - .set(&DataKey::PublicKey, &public_key); + pub fn __constructor(env: Env, signers: Vec
) { env.storage().instance().set(&DataKey::Signers, &signers); } } #[contractimpl] impl CustomAccountInterface for ModularAccount { - type Signature = BytesN<64>; + // This account holds no key of its own; it authenticates purely by + // delegating, so it carries no signature. + type Signature = (); type Error = ModularAccountError; fn __check_auth( env: Env, - signature_payload: Hash<32>, - signature: BytesN<64>, + _signature_payload: Hash<32>, + _signature: (), auth_contexts: Vec, ) -> Result<(), ModularAccountError> { - // Even though we use delegated authentication, the account can still - // perform the regular verification if necessary. - let public_key: BytesN<32> = env.storage().instance().get(&DataKey::PublicKey).unwrap(); - env.crypto() - .ed25519_verify(&public_key, &signature_payload.into(), &signature); record_authorized_calls(&env, &auth_contexts); // The signers the user attached to the auth entry for this account's diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index e3bae984..6d1fecb1 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -14,7 +14,7 @@ use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contractimpl, crypto::Hash, - vec, Address, BytesN, Env, Symbol, TryFromVal, Vec, + symbol_short, vec, Address, BytesN, Env, Symbol, TryFromVal, Vec, }; use crate::{record_authorized_calls, DataKey, ModularAccount}; @@ -31,7 +31,7 @@ impl DelegateAccount { pub fn __constructor(env: Env, public_key: BytesN<32>) { env.storage() .instance() - .set(&DataKey::PublicKey, &public_key); + .set(&symbol_short!("pubkey"), &public_key); } } @@ -46,7 +46,11 @@ impl CustomAccountInterface for DelegateAccount { signature: BytesN<64>, auth_contexts: Vec, ) -> Result<(), soroban_sdk::Error> { - let public_key: BytesN<32> = env.storage().instance().get(&DataKey::PublicKey).unwrap(); + let public_key: BytesN<32> = env + .storage() + .instance() + .get(&symbol_short!("pubkey")) + .unwrap(); env.crypto() .ed25519_verify(&public_key, &signature_payload.into(), &signature); record_authorized_calls(&env, &auth_contexts); @@ -74,7 +78,6 @@ fn sign(env: &Env, key: &SigningKey, payload: &[u8; 32]) -> ScVal { fn test_delegate_auth() { let env = Env::default(); - let account_key = SigningKey::from_bytes(&[1u8; 32]); let key_a = SigningKey::from_bytes(&[2u8; 32]); let key_b = SigningKey::from_bytes(&[3u8; 32]); @@ -87,13 +90,11 @@ fn test_delegate_auth() { (BytesN::from_array(&env, &key_b.verifying_key().to_bytes()),), ); - // Register the account with its own key and both delegates as signers. + // Register the account with both delegates as signers. The account holds no + // key of its own and authenticates purely by delegating. let account = env.register( ModularAccount, - ( - BytesN::from_array(&env, &account_key.verifying_key().to_bytes()), - vec![&env, delegate_a.clone(), delegate_b.clone()], - ), + (vec![&env, delegate_a.clone(), delegate_b.clone()],), ); let protected = env.register(Protected, ()); @@ -162,7 +163,8 @@ fn test_delegate_auth() { address: account_addr.clone(), nonce, signature_expiration_ledger, - signature: sign(&env, &account_key, &payload), + // The account has no signature of its own. + signature: ScVal::Void, }, delegates: bad_delegates.try_into().unwrap(), }, @@ -200,10 +202,8 @@ fn test_delegate_auth() { address: account_addr.clone(), nonce, signature_expiration_ledger, - // Also include the account's own signature in the - // credentials, as it is required by `ModularAccount`'s own - // verification step. - signature: sign(&env, &account_key, &payload), + // The account has no signature of its own. + signature: ScVal::Void, }, delegates: delegates.try_into().unwrap(), }, From cdba7795313713b6b222b2f8d559ab99902a1e48 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:02:31 +0000 Subject: [PATCH 04/15] assert on full auth context, not just fn names --- modular_account/src/lib.rs | 26 ++++++++------------------ modular_account/src/test.rs | 27 +++++++++++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 6d0e614f..77a331b4 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -20,7 +20,7 @@ use soroban_sdk::{ auth::{Context, CustomAccountInterface}, contract, contracterror, contractimpl, contracttype, crypto::Hash, - Address, Env, Symbol, Vec, + Address, Env, Vec, }; #[contracterror] @@ -34,9 +34,9 @@ pub enum ModularAccountError { pub enum DataKey { // The set of addresses allowed to act as delegates for this account. Signers, - // A log of the function names this account has approved, used by the tests + // The authorization contexts this account has approved, used by the tests // to verify what was authorized. - AuthorizedCalls, + AuthorizedContexts, } // A custom account that can delegate authentication to a set of registered @@ -64,7 +64,7 @@ impl CustomAccountInterface for ModularAccount { _signature: (), auth_contexts: Vec, ) -> Result<(), ModularAccountError> { - record_authorized_calls(&env, &auth_contexts); + record_authorized_contexts(&env, &auth_contexts); // The signers the user attached to the auth entry for this account's // authorization. These are unsanitized user input, so the account must @@ -89,22 +89,12 @@ impl CustomAccountInterface for ModularAccount { } } -// Appends the function name from each contract-call context to a per-account -// log in instance storage so the tests can verify what the account approved. -fn record_authorized_calls(env: &Env, auth_contexts: &Vec) { - let mut calls: Vec = env - .storage() - .instance() - .get(&DataKey::AuthorizedCalls) - .unwrap_or_else(|| Vec::new(env)); - for ctx in auth_contexts.iter() { - if let Context::Contract(c) = ctx { - calls.push_back(c.fn_name); - } - } +// Stores the full set of authorization contexts this account approved in +// instance storage so the tests can assert on the entire auth context. +fn record_authorized_contexts(env: &Env, auth_contexts: &Vec) { env.storage() .instance() - .set(&DataKey::AuthorizedCalls, &calls); + .set(&DataKey::AuthorizedContexts, auth_contexts); } mod test; diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 6d1fecb1..ef773d19 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -11,13 +11,13 @@ use soroban_sdk::xdr::{ SorobanCredentials, SorobanDelegateSignature, StringM, VecM, WriteXdr, }; use soroban_sdk::{ - auth::{Context, CustomAccountInterface}, + auth::{Context, ContractContext, CustomAccountInterface}, contract, contractimpl, crypto::Hash, - symbol_short, vec, Address, BytesN, Env, Symbol, TryFromVal, Vec, + symbol_short, vec, Address, BytesN, Env, IntoVal, Symbol, TryFromVal, Vec, }; -use crate::{record_authorized_calls, DataKey, ModularAccount}; +use crate::{record_authorized_contexts, DataKey, ModularAccount}; // An account that performs ed25519 verification and is used as a delegate // signer of `ModularAccount`. Any address type (G- or C-) that implements @@ -53,7 +53,7 @@ impl CustomAccountInterface for DelegateAccount { .unwrap(); env.crypto() .ed25519_verify(&public_key, &signature_payload.into(), &signature); - record_authorized_calls(&env, &auth_contexts); + record_authorized_contexts(&env, &auth_contexts); Ok(()) } } @@ -219,16 +219,23 @@ fn test_delegate_auth() { // only `set_auths` plus a wrapper call can be used to test the full flow. ProtectedClient::new(&env, &protected).protected(&account); - // Both the account and its delegates observe a single call to `protected` - // in their authorization contexts. - let expected = vec![&env, Symbol::new(&env, "protected")]; + // The account and both delegates each observe the same single authorization + // context: the call to `protected` with the account as its argument. + let expected = vec![ + &env, + Context::Contract(ContractContext { + contract: protected.clone(), + fn_name: Symbol::new(&env, "protected"), + args: (account.clone(),).into_val(&env), + }), + ]; for addr in [&account, &delegate_a, &delegate_b] { - let calls: Vec = env.as_contract(addr, || { + let contexts: Vec = env.as_contract(addr, || { env.storage() .instance() - .get(&DataKey::AuthorizedCalls) + .get(&DataKey::AuthorizedContexts) .unwrap() }); - assert_eq!(calls, expected); + assert!(contexts == expected); } } From fa87d95865e4b5af7934dd86a678ef4b122df9eb Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:10:30 +0000 Subject: [PATCH 05/15] rewrite modular_account to match sdk delegate auth example --- modular_account/Cargo.lock | 2 - modular_account/Cargo.toml | 2 - modular_account/src/lib.rs | 84 ++++------- modular_account/src/test.rs | 286 ++++++++++++------------------------ 4 files changed, 127 insertions(+), 247 deletions(-) diff --git a/modular_account/Cargo.lock b/modular_account/Cargo.lock index 29029e98..2b51a482 100644 --- a/modular_account/Cargo.lock +++ b/modular_account/Cargo.lock @@ -1449,8 +1449,6 @@ dependencies = [ name = "soroban-modular-account-contract" version = "0.0.0" dependencies = [ - "ed25519-dalek", - "sha2", "soroban-sdk", ] diff --git a/modular_account/Cargo.toml b/modular_account/Cargo.toml index 7f0ec258..64099e15 100644 --- a/modular_account/Cargo.toml +++ b/modular_account/Cargo.toml @@ -14,8 +14,6 @@ soroban-sdk = { version = "27.0.0-rc.1" } [dev-dependencies] soroban-sdk = { version = "27.0.0-rc.1", features = ["testutils"] } -ed25519-dalek = { version = "2.1.1" } -sha2 = { version = "0.10.8" } [profile.release] opt-level = "z" diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 77a331b4..1f0963f3 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -1,19 +1,3 @@ -//! This is a modular account contract that delegates (part of) its -//! authentication to a set of registered signer addresses. -//! -//! Instead of performing all of the signature verification itself, the -//! `ModularAccount` contract forwards its `__check_auth` context to one or more -//! registered delegate signers. Each delegate then runs its own `__check_auth` -//! independently. The user chooses which of the registered signers to -//! authenticate with by attaching them to the transaction's authorization -//! payload as delegated signers. -//! -//! Auth delegation was introduced in soroban-sdk v27 via CAP-71: -//! . -//! -//! For a single-key account see the `simple_account` example, and for a -//! multi-sig account with custom authorization policies see the `account` -//! example. #![no_std] use soroban_sdk::{ @@ -26,75 +10,65 @@ use soroban_sdk::{ #[contracterror] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(u32)] -pub enum ModularAccountError { +pub enum Error { UnknownDelegate = 1, } #[contracttype] -pub enum DataKey { - // The set of addresses allowed to act as delegates for this account. - Signers, - // The authorization contexts this account has approved, used by the tests - // to verify what was authorized. - AuthorizedContexts, +enum ModularAccountDataKey { + // Marks an address as a signer allowed to authenticate for the + // modular account. + Signer(Address), } -// A custom account that can delegate authentication to a set of registered -// signer addresses. #[contract] pub struct ModularAccount; #[contractimpl] impl ModularAccount { + // Registers the addresses allowed to authenticate for this account. pub fn __constructor(env: Env, signers: Vec
) { - env.storage().instance().set(&DataKey::Signers, &signers); + for signer in signers.iter() { + env.storage() + .persistent() + .set(&ModularAccountDataKey::Signer(signer), &()); + } } } #[contractimpl] impl CustomAccountInterface for ModularAccount { - // This account holds no key of its own; it authenticates purely by - // delegating, so it carries no signature. + // The account verifies no signature of its own, so it carries no + // signature to check. type Signature = (); - type Error = ModularAccountError; + type Error = Error; fn __check_auth( env: Env, _signature_payload: Hash<32>, - _signature: (), - auth_contexts: Vec, - ) -> Result<(), ModularAccountError> { - record_authorized_contexts(&env, &auth_contexts); - - // The signers the user attached to the auth entry for this account's - // authorization. These are unsanitized user input, so the account must - // verify each one against its own registered signers below. + _signatures: (), + _auth_contexts: Vec, + ) -> Result<(), Error> { + // The signers the user attached to the auth entry for this + // account's authorization. let delegates = env.custom_account().get_delegated_signers(); - let signers: Vec
= env.storage().instance().get(&DataKey::Signers).unwrap(); + // Check if the delegates are accepted by the modular account. for delegate in delegates.iter() { - // The host can not validate the delegates, so the account has to - // check that each one is actually a registered signer. - if !signers.contains(&delegate) { - return Err(ModularAccountError::UnknownDelegate); + if !env + .storage() + .persistent() + .has(&ModularAccountDataKey::Signer(delegate.clone())) + { + return Err(Error::UnknownDelegate); } - // Forward the current authentication context to the delegate. Unlike - // `require_auth`, this does not start a new contract invocation and - // does not require a separate auth entry for the delegate in the - // transaction. Delegation is nestable: a delegate may further - // delegate. + } + // Forward the current authorization to each delegate. + for delegate in delegates.iter() { env.custom_account().delegate_auth(&delegate); } Ok(()) } } -// Stores the full set of authorization contexts this account approved in -// instance storage so the tests can assert on the entire auth context. -fn record_authorized_contexts(env: &Env, auth_contexts: &Vec) { - env.storage() - .instance() - .set(&DataKey::AuthorizedContexts, auth_contexts); -} - mod test; diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index ef773d19..4092f99e 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -1,59 +1,51 @@ #![cfg(test)] extern crate std; -use ed25519_dalek::{Signer, SigningKey}; -use sha2::{Digest, Sha256}; - -use soroban_sdk::xdr::{ - HashIdPreimage, HashIdPreimageSorobanAuthorizationWithAddress, InvokeContractArgs, Limits, - ScAddress, ScVal, SorobanAddressCredentials, SorobanAddressCredentialsWithDelegates, - SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, - SorobanCredentials, SorobanDelegateSignature, StringM, VecM, WriteXdr, -}; use soroban_sdk::{ auth::{Context, ContractContext, CustomAccountInterface}, - contract, contractimpl, + contract, contractimpl, contracttype, crypto::Hash, - symbol_short, vec, Address, BytesN, Env, IntoVal, Symbol, TryFromVal, Vec, + testutils::{AuthorizedFunction, AuthorizedInvocation}, + vec, Address, Env, IntoVal, Symbol, Vec, }; +use soroban_sdk::xdr::{ + InvokeContractArgs, ScAddress, ScVal, SorobanAddressCredentials, + SorobanAddressCredentialsWithDelegates, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, + SorobanDelegateSignature, StringM, VecM, +}; + +use crate::{Error, ModularAccount}; -use crate::{record_authorized_contexts, DataKey, ModularAccount}; +#[contracttype] +enum DelegateAccountDataKey { + // Records the contexts the delegate approved so the test can verify + // the delegation reached it. + ApprovedContexts, +} -// An account that performs ed25519 verification and is used as a delegate -// signer of `ModularAccount`. Any address type (G- or C-) that implements -// `CustomAccountInterface` can be a delegate, so this is defined here as a -// test fixture rather than as a deployable contract of this crate. +// A simple account that the ModularAccount can delegate to for auth. +// +// It will always authorize an auth request, and store a copy of the auth +// context for later comparing in tests. #[contract] pub struct DelegateAccount; -#[contractimpl] -impl DelegateAccount { - pub fn __constructor(env: Env, public_key: BytesN<32>) { - env.storage() - .instance() - .set(&symbol_short!("pubkey"), &public_key); - } -} - #[contractimpl] impl CustomAccountInterface for DelegateAccount { - type Signature = BytesN<64>; - type Error = soroban_sdk::Error; - + type Signature = (); + type Error = Error; fn __check_auth( env: Env, - signature_payload: Hash<32>, - signature: BytesN<64>, + _signature_payload: Hash<32>, + _signatures: (), auth_contexts: Vec, - ) -> Result<(), soroban_sdk::Error> { - let public_key: BytesN<32> = env - .storage() + ) -> Result<(), Error> { + env.storage() .instance() - .get(&symbol_short!("pubkey")) - .unwrap(); - env.crypto() - .ed25519_verify(&public_key, &signature_payload.into(), &signature); - record_authorized_contexts(&env, &auth_contexts); + .set(&DelegateAccountDataKey::ApprovedContexts, &auth_contexts); + // Returning `Ok(())` approves the auth; + // returning an error would reject it. Ok(()) } } @@ -69,173 +61,91 @@ impl Protected { } } -fn sign(env: &Env, key: &SigningKey, payload: &[u8; 32]) -> ScVal { - let sig: [u8; 64] = key.sign(payload).to_bytes(); - ScVal::try_from_val(env, &BytesN::from_array(env, &sig).to_val()).unwrap() -} - #[test] -fn test_delegate_auth() { +fn test() { let env = Env::default(); - - let key_a = SigningKey::from_bytes(&[2u8; 32]); - let key_b = SigningKey::from_bytes(&[3u8; 32]); - - let delegate_a = env.register( - DelegateAccount, - (BytesN::from_array(&env, &key_a.verifying_key().to_bytes()),), - ); - let delegate_b = env.register( - DelegateAccount, - (BytesN::from_array(&env, &key_b.verifying_key().to_bytes()),), - ); - - // Register the account with both delegates as signers. The account holds no - // key of its own and authenticates purely by delegating. - let account = env.register( - ModularAccount, - (vec![&env, delegate_a.clone(), delegate_b.clone()],), - ); + let delegate = env.register(DelegateAccount, ()); + // Register the modular account with `delegate` as an allowed signer. + let account = env.register(ModularAccount, (vec![&env, delegate.clone()],)); let protected = env.register(Protected, ()); - let account_addr: ScAddress = account.clone().into(); + let account_addr: ScAddress = account.clone().try_into().unwrap(); + let delegate_addr: ScAddress = delegate.clone().try_into().unwrap(); - let nonce = 123; - let signature_expiration_ledger = 100; - - // Create authorized invocation for the `protected` call. - let root_invocation = SorobanAuthorizedInvocation { - function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { - contract_address: protected.clone().into(), - function_name: StringM::try_from("protected").unwrap().into(), - args: std::vec![ScVal::Address(account_addr.clone())] - .try_into() - .unwrap(), - }), - sub_invocations: VecM::default(), - }; - - // Build the signature payload by hashing the - // `HashIdPreimage::SorobanAuthorizationWithAddress` preimage required for - // `AddressWithDelegates` credentials. - let network_id = env.ledger().network_id(); - let preimage = HashIdPreimage::SorobanAuthorizationWithAddress( - HashIdPreimageSorobanAuthorizationWithAddress { - network_id: network_id.to_array().into(), - nonce, - signature_expiration_ledger, - invocation: root_invocation.clone(), - address: account_addr.clone(), - }, - ); - let preimage_xdr = preimage.to_xdr(Limits::none()).unwrap(); - let payload: [u8; 32] = Sha256::digest(&preimage_xdr).into(); - - // Negative scenario: attach a delegate signer that isn't registered with the - // account. The account should reject the authorization with an - // `UnknownDelegate` error. - let unknown_key = SigningKey::from_bytes(&[4u8; 32]); - let unknown_delegate = env.register( - DelegateAccount, - (BytesN::from_array( - &env, - &unknown_key.verifying_key().to_bytes(), - ),), - ); - let mut bad_delegates = std::vec![ - SorobanDelegateSignature { - address: delegate_a.clone().into(), - signature: sign(&env, &key_a, &payload), - nested_delegates: VecM::default(), - }, - SorobanDelegateSignature { - address: unknown_delegate.clone().into(), - signature: sign(&env, &unknown_key, &payload), - nested_delegates: VecM::default(), - }, - ]; - // Delegates must be sorted by address. - bad_delegates.sort_by(|x, y| x.address.cmp(&y.address)); + // This authorization entry is normally built by the user's + // wallet/tooling and attached to the transaction. It authorizes + // `protected` on behalf of the account, and attaches `delegate` as a + // delegated signer. Delegates must be sorted by address with no + // duplicates. env.set_auths(&[SorobanAuthorizationEntry { credentials: SorobanCredentials::AddressWithDelegates( SorobanAddressCredentialsWithDelegates { address_credentials: SorobanAddressCredentials { address: account_addr.clone(), - nonce, - signature_expiration_ledger, - // The account has no signature of its own. + nonce: 1, + signature_expiration_ledger: 100, + // The account verifies no signature of its own. signature: ScVal::Void, }, - delegates: bad_delegates.try_into().unwrap(), - }, - ), - root_invocation: root_invocation.clone(), - }]); - // The call will fail due to the auth failure. - assert!(ProtectedClient::new(&env, &protected) - .try_protected(&account) - .is_err()); - - // Positive scenario: each registered delegate signs the same payload with - // its own distinct key. - let mut delegates = std::vec![ - SorobanDelegateSignature { - address: delegate_a.clone().into(), - signature: sign(&env, &key_a, &payload), - nested_delegates: VecM::default(), - }, - SorobanDelegateSignature { - address: delegate_b.clone().into(), - signature: sign(&env, &key_b, &payload), - nested_delegates: VecM::default(), - }, - ]; - // Delegates must be sorted by address. - delegates.sort_by(|x, y| x.address.cmp(&y.address)); - - // Build the full authorization entry with `AddressWithDelegates` - // credentials containing both delegates. - env.set_auths(&[SorobanAuthorizationEntry { - credentials: SorobanCredentials::AddressWithDelegates( - SorobanAddressCredentialsWithDelegates { - address_credentials: SorobanAddressCredentials { - address: account_addr.clone(), - nonce, - signature_expiration_ledger, - // The account has no signature of its own. + delegates: std::vec![SorobanDelegateSignature { + address: delegate_addr, signature: ScVal::Void, - }, - delegates: delegates.try_into().unwrap(), + nested_delegates: VecM::default(), + }] + .try_into() + .unwrap(), }, ), - root_invocation: root_invocation.clone(), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: protected.clone().try_into().unwrap(), + function_name: StringM::try_from("protected").unwrap().into(), + args: std::vec![ScVal::Address(account_addr)].try_into().unwrap(), + }), + sub_invocations: VecM::default(), + }, }]); - // Call the `protected` function with the enforced authorization payload - // above. - // - // Note that testing delegated auth via - // `env.try_invoke_contract_check_auth` is not supported at the moment, so - // only `set_auths` plus a wrapper call can be used to test the full flow. + // The call succeeds: the account delegates its authentication to + // `delegate`, which approves it. ProtectedClient::new(&env, &protected).protected(&account); - // The account and both delegates each observe the same single authorization - // context: the call to `protected` with the account as its argument. - let expected = vec![ - &env, - Context::Contract(ContractContext { - contract: protected.clone(), - fn_name: Symbol::new(&env, "protected"), - args: (account.clone(),).into_val(&env), - }), - ]; - for addr in [&account, &delegate_a, &delegate_b] { - let contexts: Vec = env.as_contract(addr, || { - env.storage() - .instance() - .get(&DataKey::AuthorizedContexts) - .unwrap() - }); - assert!(contexts == expected); - } + // The account authorized the `protected` call. Delegating to + // `delegate` is not recorded as a separate authorization. + assert_eq!( + env.auths(), + std::vec![( + account.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + protected.clone(), + Symbol::new(&env, "protected"), + (account.clone(),).into_val(&env), + )), + sub_invocations: std::vec![], + } + )] + ); + + // The delegation actually reached `delegate`, which approved the + // same invocation that was authorized above. + let approved: Vec = env.as_contract(&delegate, || { + env.storage() + .instance() + .get(&DelegateAccountDataKey::ApprovedContexts) + .unwrap() + }); + // `Context` does not implement `Debug` in soroban-sdk 27.0.0-rc.1, so this + // compares with `==` instead of `assert_eq!`. + assert!( + approved + == vec![ + &env, + Context::Contract(ContractContext { + contract: protected.clone(), + fn_name: Symbol::new(&env, "protected"), + args: (account.clone(),).into_val(&env), + }), + ], + ); } From 5d1a3c585c9e682f3043be6f07883da2ec0b802b Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:11:27 +0000 Subject: [PATCH 06/15] apply rustfmt to modular_account test --- modular_account/src/test.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 4092f99e..0949ea4e 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -1,6 +1,11 @@ #![cfg(test)] extern crate std; +use soroban_sdk::xdr::{ + InvokeContractArgs, ScAddress, ScVal, SorobanAddressCredentials, + SorobanAddressCredentialsWithDelegates, SorobanAuthorizationEntry, SorobanAuthorizedFunction, + SorobanAuthorizedInvocation, SorobanCredentials, SorobanDelegateSignature, StringM, VecM, +}; use soroban_sdk::{ auth::{Context, ContractContext, CustomAccountInterface}, contract, contractimpl, contracttype, @@ -8,12 +13,6 @@ use soroban_sdk::{ testutils::{AuthorizedFunction, AuthorizedInvocation}, vec, Address, Env, IntoVal, Symbol, Vec, }; -use soroban_sdk::xdr::{ - InvokeContractArgs, ScAddress, ScVal, SorobanAddressCredentials, - SorobanAddressCredentialsWithDelegates, SorobanAuthorizationEntry, - SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, - SorobanDelegateSignature, StringM, VecM, -}; use crate::{Error, ModularAccount}; From a4818150b71563db5fa413d976ff83f1afb96e27 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:27:47 +1000 Subject: [PATCH 07/15] Update lib.rs --- modular_account/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 1f0963f3..69884c23 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -63,10 +63,12 @@ impl CustomAccountInterface for ModularAccount { return Err(Error::UnknownDelegate); } } + // Forward the current authorization to each delegate. for delegate in delegates.iter() { env.custom_account().delegate_auth(&delegate); } + Ok(()) } } From 6e4fe00f2e09f73e8481f100ef6bbb3e8b93e9a0 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:28:10 +1000 Subject: [PATCH 08/15] Remove unnecessary whitespace in delegate authorization --- modular_account/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 69884c23..a4c3cec4 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -63,12 +63,12 @@ impl CustomAccountInterface for ModularAccount { return Err(Error::UnknownDelegate); } } - + // Forward the current authorization to each delegate. for delegate in delegates.iter() { env.custom_account().delegate_auth(&delegate); } - + Ok(()) } } From 8d187fbcfac37bc9b3a182cdb5492d43a414c458 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:17:05 +0000 Subject: [PATCH 09/15] bump soroban-sdk to released 27.0.0 --- modular_account/Cargo.lock | 20 ++++++++++---------- modular_account/Cargo.toml | 4 ++-- modular_account/src/test.rs | 22 ++++++++++------------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/modular_account/Cargo.lock b/modular_account/Cargo.lock index 2b51a482..0bef87d5 100644 --- a/modular_account/Cargo.lock +++ b/modular_account/Cargo.lock @@ -1433,9 +1433,9 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "27.0.0-rc.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2272e9ee6f44cae1f3fe38b16f83e37ae6b55d002ecd7cbd45e02fde277c5a0" +checksum = "1e4bf41f85da648dca4153ca5ed41411658d2029e15d9ba693fdd2e3e5dbe05d" dependencies = [ "serde", "serde_json", @@ -1454,9 +1454,9 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "27.0.0-rc.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de4cf66a6ec501ca3bb6018e36d3293ffbcd9b83651d8ae5afa03a4c8dd02fd1" +checksum = "a5636f9d62dd0cc6d23f08f775aa60f712ec596e5761f6911aee154337d401bf" dependencies = [ "arbitrary", "bytes-lit", @@ -1478,9 +1478,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "27.0.0-rc.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ecdb16404a2a973c88193ccbfe519aa44b02cd2ff838dd61ce49117a2537c" +checksum = "f7c9f001d91196d9cd659634f9113f75609819204aef5fae27b4ab845cdfc741" dependencies = [ "darling 0.20.11", "heck", @@ -1498,9 +1498,9 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "27.0.0-rc.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85665c21947b7e9fff81a8e81c866e2e774040039856f75af9cb06ec75191c8" +checksum = "ca1e22ce713871c6f9d21a321051b58e53080629b6a5be1132cf11f42cca4d10" dependencies = [ "base64", "sha2", @@ -1511,9 +1511,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "27.0.0-rc.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb924883f555ca020951e861025d153657481e0a88dddec9f853e2aad76ddc9" +checksum = "89fb158f79ec527e46a5d5162f11c4269e17748e6b69391cdf648133d76cfead" dependencies = [ "prettyplease", "proc-macro2", diff --git a/modular_account/Cargo.toml b/modular_account/Cargo.toml index 64099e15..0d1f3250 100644 --- a/modular_account/Cargo.toml +++ b/modular_account/Cargo.toml @@ -10,10 +10,10 @@ crate-type = ["cdylib"] doctest = false [dependencies] -soroban-sdk = { version = "27.0.0-rc.1" } +soroban-sdk = { version = "27.0.0" } [dev-dependencies] -soroban-sdk = { version = "27.0.0-rc.1", features = ["testutils"] } +soroban-sdk = { version = "27.0.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 0949ea4e..8601bbd8 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -134,17 +134,15 @@ fn test() { .get(&DelegateAccountDataKey::ApprovedContexts) .unwrap() }); - // `Context` does not implement `Debug` in soroban-sdk 27.0.0-rc.1, so this - // compares with `==` instead of `assert_eq!`. - assert!( - approved - == vec![ - &env, - Context::Contract(ContractContext { - contract: protected.clone(), - fn_name: Symbol::new(&env, "protected"), - args: (account.clone(),).into_val(&env), - }), - ], + assert_eq!( + approved, + vec![ + &env, + Context::Contract(ContractContext { + contract: protected.clone(), + fn_name: Symbol::new(&env, "protected"), + args: (account.clone(),).into_val(&env), + }), + ], ); } From 54473c1db99785ba9c72a2b09055fa02b613c8fe Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:21:00 +0000 Subject: [PATCH 10/15] reject empty delegate list in __check_auth --- modular_account/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index a4c3cec4..1d090dd5 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -53,6 +53,12 @@ impl CustomAccountInterface for ModularAccount { // account's authorization. let delegates = env.custom_account().get_delegated_signers(); + // With no delegates to forward to, the account would authenticate + // nothing and be effectively unauthenticated, so reject it. + if delegates.is_empty() { + return Err(Error::UnknownDelegate); + } + // Check if the delegates are accepted by the modular account. for delegate in delegates.iter() { if !env From 1f27112231a4a072f6ab0ced8b9496d5b11d716b Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:10:47 +0000 Subject: [PATCH 11/15] add negative tests for delegate rejection paths --- modular_account/src/test.rs | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 8601bbd8..ec3b5da2 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -146,3 +146,80 @@ fn test() { ], ); } + +// Builds and sets an auth entry that calls `protected` on behalf of +// `account`, attaching `delegates` as the account's delegated signers. +fn set_delegated_auth( + env: &Env, + account: &Address, + protected: &Address, + delegates: std::vec::Vec, +) { + let account_addr: ScAddress = account.clone().try_into().unwrap(); + env.set_auths(&[SorobanAuthorizationEntry { + credentials: SorobanCredentials::AddressWithDelegates( + SorobanAddressCredentialsWithDelegates { + address_credentials: SorobanAddressCredentials { + address: account_addr.clone(), + nonce: 1, + signature_expiration_ledger: 100, + signature: ScVal::Void, + }, + delegates: delegates + .into_iter() + .map(|address| SorobanDelegateSignature { + address, + signature: ScVal::Void, + nested_delegates: VecM::default(), + }) + .collect::>() + .try_into() + .unwrap(), + }, + ), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: protected.clone().try_into().unwrap(), + function_name: StringM::try_from("protected").unwrap().into(), + args: std::vec![ScVal::Address(account_addr)].try_into().unwrap(), + }), + sub_invocations: VecM::default(), + }, + }]); +} + +// A delegate the account has not registered is rejected with +// `UnknownDelegate`, so the protected call fails. +#[test] +fn test_unknown_delegate_is_rejected() { + let env = Env::default(); + let delegate = env.register(DelegateAccount, ()); + let stranger = env.register(DelegateAccount, ()); + let account = env.register(ModularAccount, (vec![&env, delegate],)); + let protected = env.register(Protected, ()); + + set_delegated_auth( + &env, + &account, + &protected, + std::vec![stranger.try_into().unwrap()], + ); + + let res = ProtectedClient::new(&env, &protected).try_protected(&account); + assert!(res.is_err()); +} + +// An auth entry with no delegates would leave the account authenticating +// nothing, so it is rejected and the protected call fails. +#[test] +fn test_empty_delegates_is_rejected() { + let env = Env::default(); + let delegate = env.register(DelegateAccount, ()); + let account = env.register(ModularAccount, (vec![&env, delegate],)); + let protected = env.register(Protected, ()); + + set_delegated_auth(&env, &account, &protected, std::vec![]); + + let res = ProtectedClient::new(&env, &protected).try_protected(&account); + assert!(res.is_err()); +} From a639fa67d8b1a661f3b7963899c83427f34bd133 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:09:57 +0000 Subject: [PATCH 12/15] use InsufficientDelegates error for empty delegate list --- modular_account/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modular_account/src/lib.rs b/modular_account/src/lib.rs index 1d090dd5..28dd5991 100644 --- a/modular_account/src/lib.rs +++ b/modular_account/src/lib.rs @@ -12,6 +12,7 @@ use soroban_sdk::{ #[repr(u32)] pub enum Error { UnknownDelegate = 1, + InsufficientDelegates = 2, } #[contracttype] @@ -54,9 +55,10 @@ impl CustomAccountInterface for ModularAccount { let delegates = env.custom_account().get_delegated_signers(); // With no delegates to forward to, the account would authenticate - // nothing and be effectively unauthenticated, so reject it. + // nothing and be effectively unauthenticated, so reject it. A real + // account might require more than one delegate to meet a threshold. if delegates.is_empty() { - return Err(Error::UnknownDelegate); + return Err(Error::InsufficientDelegates); } // Check if the delegates are accepted by the modular account. From 70993a3248f7641939f6101331b12ca01132e49f Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:37:58 +0000 Subject: [PATCH 13/15] inline auth entry construction in negative tests --- modular_account/src/test.rs | 79 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index ec3b5da2..6e41a97a 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -147,15 +147,19 @@ fn test() { ); } -// Builds and sets an auth entry that calls `protected` on behalf of -// `account`, attaching `delegates` as the account's delegated signers. -fn set_delegated_auth( - env: &Env, - account: &Address, - protected: &Address, - delegates: std::vec::Vec, -) { +// A delegate the account has not registered is rejected with +// `UnknownDelegate`, so the protected call fails. +#[test] +fn test_unknown_delegate_is_rejected() { + let env = Env::default(); + let delegate = env.register(DelegateAccount, ()); + let stranger = env.register(DelegateAccount, ()); + let account = env.register(ModularAccount, (vec![&env, delegate],)); + let protected = env.register(Protected, ()); + let account_addr: ScAddress = account.clone().try_into().unwrap(); + let stranger_addr: ScAddress = stranger.try_into().unwrap(); + env.set_auths(&[SorobanAuthorizationEntry { credentials: SorobanCredentials::AddressWithDelegates( SorobanAddressCredentialsWithDelegates { @@ -165,16 +169,13 @@ fn set_delegated_auth( signature_expiration_ledger: 100, signature: ScVal::Void, }, - delegates: delegates - .into_iter() - .map(|address| SorobanDelegateSignature { - address, - signature: ScVal::Void, - nested_delegates: VecM::default(), - }) - .collect::>() - .try_into() - .unwrap(), + delegates: std::vec![SorobanDelegateSignature { + address: stranger_addr, + signature: ScVal::Void, + nested_delegates: VecM::default(), + }] + .try_into() + .unwrap(), }, ), root_invocation: SorobanAuthorizedInvocation { @@ -186,24 +187,6 @@ fn set_delegated_auth( sub_invocations: VecM::default(), }, }]); -} - -// A delegate the account has not registered is rejected with -// `UnknownDelegate`, so the protected call fails. -#[test] -fn test_unknown_delegate_is_rejected() { - let env = Env::default(); - let delegate = env.register(DelegateAccount, ()); - let stranger = env.register(DelegateAccount, ()); - let account = env.register(ModularAccount, (vec![&env, delegate],)); - let protected = env.register(Protected, ()); - - set_delegated_auth( - &env, - &account, - &protected, - std::vec![stranger.try_into().unwrap()], - ); let res = ProtectedClient::new(&env, &protected).try_protected(&account); assert!(res.is_err()); @@ -218,7 +201,29 @@ fn test_empty_delegates_is_rejected() { let account = env.register(ModularAccount, (vec![&env, delegate],)); let protected = env.register(Protected, ()); - set_delegated_auth(&env, &account, &protected, std::vec![]); + let account_addr: ScAddress = account.clone().try_into().unwrap(); + + env.set_auths(&[SorobanAuthorizationEntry { + credentials: SorobanCredentials::AddressWithDelegates( + SorobanAddressCredentialsWithDelegates { + address_credentials: SorobanAddressCredentials { + address: account_addr.clone(), + nonce: 1, + signature_expiration_ledger: 100, + signature: ScVal::Void, + }, + delegates: VecM::default(), + }, + ), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: protected.clone().try_into().unwrap(), + function_name: StringM::try_from("protected").unwrap().into(), + args: std::vec![ScVal::Address(account_addr)].try_into().unwrap(), + }), + sub_invocations: VecM::default(), + }, + }]); let res = ProtectedClient::new(&env, &protected).try_protected(&account); assert!(res.is_err()); From 01dda0421cbabd0e670b54c58fc42864e73b28b0 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:42:12 +0000 Subject: [PATCH 14/15] assert exact auth error in negative tests --- modular_account/src/test.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 6e41a97a..41222333 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -2,7 +2,7 @@ extern crate std; use soroban_sdk::xdr::{ - InvokeContractArgs, ScAddress, ScVal, SorobanAddressCredentials, + InvokeContractArgs, ScAddress, ScErrorCode, ScErrorType, ScVal, SorobanAddressCredentials, SorobanAddressCredentialsWithDelegates, SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, SorobanDelegateSignature, StringM, VecM, }; @@ -189,7 +189,15 @@ fn test_unknown_delegate_is_rejected() { }]); let res = ProtectedClient::new(&env, &protected).try_protected(&account); - assert!(res.is_err()); + // A failed `__check_auth` surfaces as a generic auth error, not the + // account's own error code. + assert_eq!( + res, + Err(Ok(soroban_sdk::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InvalidAction, + ))) + ); } // An auth entry with no delegates would leave the account authenticating @@ -226,5 +234,13 @@ fn test_empty_delegates_is_rejected() { }]); let res = ProtectedClient::new(&env, &protected).try_protected(&account); - assert!(res.is_err()); + // A failed `__check_auth` surfaces as a generic auth error, not the + // account's own error code. + assert_eq!( + res, + Err(Ok(soroban_sdk::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InvalidAction, + ))) + ); } From 9aabcde3ea64f8b3f4be54a944b00535fdb05bbc Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:53:46 +0000 Subject: [PATCH 15/15] assert account error code via diagnostic events in negative tests --- modular_account/src/test.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/modular_account/src/test.rs b/modular_account/src/test.rs index 41222333..7d56d516 100644 --- a/modular_account/src/test.rs +++ b/modular_account/src/test.rs @@ -147,6 +147,22 @@ fn test() { ); } +// Asserts the host recorded a diagnostic event carrying the account's own +// `__check_auth` error code. The caller only sees the generic auth failure, +// but the underlying `Error(Contract, #)` is captured in the host's +// diagnostic events, formatted here and matched by substring. +fn assert_check_auth_error(env: &Env, expected: Error) { + let needle = std::format!("Error(Contract, #{})", expected as u32); + let found = env + .host() + .get_diagnostic_events() + .unwrap() + .0 + .iter() + .any(|event| std::format!("{event}").contains(&needle)); + assert!(found, "expected a diagnostic event reporting {needle}"); +} + // A delegate the account has not registered is rejected with // `UnknownDelegate`, so the protected call fails. #[test] @@ -198,6 +214,9 @@ fn test_unknown_delegate_is_rejected() { ScErrorCode::InvalidAction, ))) ); + // The account itself rejected the unregistered delegate with its own + // `UnknownDelegate` code. + assert_check_auth_error(&env, Error::UnknownDelegate); } // An auth entry with no delegates would leave the account authenticating @@ -243,4 +262,7 @@ fn test_empty_delegates_is_rejected() { ScErrorCode::InvalidAction, ))) ); + // The account itself rejected the empty delegate list with its own + // `InsufficientDelegates` code. + assert_check_auth_error(&env, Error::InsufficientDelegates); }