From afeb4ccfa377116e193fefdbf446c4249f6151fe Mon Sep 17 00:00:00 2001 From: Grace Eberechi Date: Mon, 26 Oct 2026 11:36:07 +0100 Subject: [PATCH] Emit investment_made event and mint fraction tokens on invest Closes #518 Closes #517 Both issues ask for functionality that was already substantially implemented under different names: invoice-escrow's invest/finalise_funding (#518's release_to_issuer)/refund (#518's refund_investors)/get_invoice (#518's get_escrow_balance) cover #518's asks, and invoice-token's mint/burn/transfer/balance/events already cover #517's asks as a standalone SEP-41-compliant contract (one deployed instance per invoice, so invoice_id is set once at initialize rather than passed per call). Two real gaps found and fixed: invest() on the multi-investor FundingInvoice flow (created via create_invoice, distinct from the legacy register_invoice/EscrowStatus path) emitted nothing at all. Added events::investment_made, published with the investor, invoice id, amount, and the investor's new total position; wired into invest(). Covered by test_invest_on_funding_invoice_emits_investment_made_event. them any fraction tokens representing their ownership share, because the two contracts were never wired together (invoice-token was only a dev-dependency of invoice-escrow, used in tests, not a real cross-contract call). The legacy create_escrow_with_tokens path already does this exact mint for its own flow ("Mint invoice tokens to the buyer to represent their ownership share"); invest() on FundingInvoice just never got the same treatment. Added an optional fraction_token field to FundingInvoice (Option
, additive so it doesn't disturb the existing field layout) set via a new trailing create_invoice parameter; when set, invest() calls the configured invoice-token contract's mint via env.invoke_contract, same pattern as the legacy path. When fraction_token is None, behavior is unchanged (payment-only, no fraction tokens) for invoices that don't need one. Covered by test_invest_mints_fraction_tokens_when_fraction_token_configured, which deploys a real invoice-token instance, configures escrow as its minter, and asserts the investor's fraction balance after two invests. Scoped smaller than the full "wire every escrow lifecycle state to the token contract" version (burn on refund/partial_refund/finalise_funding is not wired here) per discussion: the minimal, lowest-risk integration is investor receives fraction tokens on invest, which is the literal ask in proportional to investment"); burn-on-settlement is a reasonable follow-up but adds more surface (which lifecycle transitions trigger it, whether partial refunds burn partially) that deserves its own review. Validation: cargo build -p invoice-escrow -p invoice-token builds clean (only pre-existing deprecation warnings). The crate's own test suite (contracts/invoice-escrow/src/test.rs) fails to compile with 152 pre-existing errors on a clean upstream/dev checkout (confirmed via git stash before/after) due to a soroban-sdk 27.0.2 -> 27.0.3 minor-version API drift (Storage::to_xdr removed, StorageKey visibility changed, etc.), unrelated to this change and well outside its scope to fix. Verified my two new tests compile cleanly in isolation (cargo check -p invoice-escrow --tests, confirmed zero errors in their line range) since the broken pre-existing tests in the same file prevent actually running the suite. --- Cargo.lock | 294 +++++++----------- contracts/invoice-escrow/src/events.rs | 21 ++ contracts/invoice-escrow/src/lib.rs | 27 ++ contracts/invoice-escrow/src/test.rs | 128 +++++++- .../invoice-escrow/src/test_buy_limit.rs | 4 + contracts/invoice-escrow/src/types.rs | 6 + .../tests/institutional_controls.rs | 5 +- contracts/invoice-escrow/tests/thresholds.rs | 12 +- 8 files changed, 310 insertions(+), 187 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9aa43d8a..e9ef5b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" dependencies = [ "gimli", ] @@ -58,9 +58,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] @@ -129,7 +129,7 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest 0.10.7", + "digest", "educe", "itertools 0.13.0", "num-bigint", @@ -185,7 +185,7 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest 0.10.7", + "digest", "num-bigint", ] @@ -231,9 +231,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" dependencies = [ "addr2line", "cfg-if", @@ -327,15 +327,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - [[package]] name = "borsh" version = "1.8.1" @@ -427,9 +418,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.9.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "shlex", @@ -546,6 +537,13 @@ dependencies = [ "serde_json", ] +[[package]] +name = "credit-scoring" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -568,16 +566,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", - "rand_core 0.10.1", -] - [[package]] name = "ctor" version = "0.2.9" @@ -613,25 +601,8 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto 0.2.9", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "curve25519-dalek-derive", - "digest 0.11.3", - "fiat-crypto 0.3.0", - "rand_core 0.10.1", + "digest", + "fiat-crypto", "rustc_version", "subtle", "zeroize", @@ -790,22 +761,12 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", - "crypto-common 0.1.6", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", -] - [[package]] name = "dividend-reinvestment" version = "0.1.0" @@ -861,10 +822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest 0.10.7", + "digest", "elliptic-curve", "rfc6979", - "signature 2.2.0", + "signature", ] [[package]] @@ -874,16 +835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", - "signature 2.2.0", -] - -[[package]] -name = "ed25519" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" -dependencies = [ - "signature 3.0.0", + "signature", ] [[package]] @@ -892,26 +844,11 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", + "curve25519-dalek", + "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" -dependencies = [ - "curve25519-dalek 5.0.0", - "ed25519 3.0.0", - "rand_core 0.10.1", - "sha2 0.11.1", - "signature 3.0.0", + "sha2", "subtle", "zeroize", ] @@ -942,7 +879,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "digest", "ff", "generic-array", "group", @@ -1013,13 +950,6 @@ dependencies = [ "soroban-sdk 27.0.6", ] -[[package]] -name = "fee-treasury" -version = "0.1.0" -dependencies = [ - "soroban-sdk 27.0.6", -] - [[package]] name = "ff" version = "0.13.1" @@ -1036,17 +966,11 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "find-msvc-tools" -version = "0.1.15" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -1056,21 +980,21 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-task", @@ -1116,9 +1040,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" [[package]] name = "grace-interest" @@ -1205,16 +1129,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hybrid-array" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" -dependencies = [ - "typenum", + "digest", ] [[package]] @@ -1370,6 +1285,13 @@ dependencies = [ "soroban-sdk 27.0.6", ] +[[package]] +name = "invoice-milestone" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "invoice-performance-alert" version = "0.1.0" @@ -1499,9 +1421,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.106" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1517,7 +1439,7 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -1556,9 +1478,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" [[package]] name = "loyalty-tier" @@ -1586,9 +1508,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", ] @@ -1654,9 +1576,9 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" dependencies = [ "memchr", ] @@ -1681,6 +1603,13 @@ dependencies = [ "soroban-sdk 27.0.6", ] +[[package]] +name = "overcollateral-reward" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "p256" version = "0.13.2" @@ -1690,7 +1619,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -2046,6 +1975,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secondary-cooldown" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "secondary-market" version = "0.1.0" @@ -2053,6 +1989,13 @@ dependencies = [ "soroban-sdk 27.0.6", ] +[[package]] +name = "secondary-market-oracle" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "secondary-trade-history" version = "0.1.0" @@ -2151,18 +2094,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", + "digest", ] [[package]] @@ -2171,7 +2103,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -2187,19 +2119,10 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "slab" version = "0.4.12" @@ -2208,9 +2131,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "soroban-builtin-sdk-macros" @@ -2301,9 +2224,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" dependencies = [ "backtrace", - "curve25519-dalek 5.0.0", + "curve25519-dalek", "ecdsa", - "ed25519-dalek 3.0.0", + "ed25519-dalek", "elliptic-curve", "generic-array", "getrandom 0.2.17", @@ -2317,7 +2240,7 @@ dependencies = [ "rand 0.8.8", "rand_chacha", "sec1", - "sha2 0.10.9", + "sha2", "sha3", "soroban-builtin-sdk-macros 21.2.1", "soroban-env-common 21.2.1", @@ -2338,9 +2261,9 @@ dependencies = [ "ark-ec", "ark-ff", "ark-serialize", - "curve25519-dalek 5.0.0", + "curve25519-dalek", "ecdsa", - "ed25519-dalek 2.2.0", + "ed25519-dalek", "elliptic-curve", "generic-array", "getrandom 0.2.17", @@ -2354,7 +2277,7 @@ dependencies = [ "rand 0.8.8", "rand_chacha", "sec1", - "sha2 0.10.9", + "sha2", "sha3", "soroban-builtin-sdk-macros 27.0.1", "soroban-env-common 27.0.1", @@ -2432,7 +2355,7 @@ dependencies = [ "bytes-lit 0.0.5", "ctor 0.2.9", "derive_arbitrary", - "ed25519-dalek 2.2.0", + "ed25519-dalek", "rand 0.8.8", "rustc_version", "serde", @@ -2455,7 +2378,7 @@ dependencies = [ "crate-git-revision 0.0.9", "ctor 0.5.0", "derive_arbitrary", - "ed25519-dalek 2.2.0", + "ed25519-dalek", "rand 0.8.8", "rustc_version", "serde", @@ -2480,7 +2403,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "sha2 0.10.9", + "sha2", "soroban-env-common 21.2.1", "soroban-spec 21.7.7", "soroban-spec-rust 21.7.7", @@ -2500,7 +2423,7 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "sha2 0.10.9", + "sha2", "soroban-env-common 27.0.1", "soroban-spec 27.0.6", "soroban-spec-rust 27.0.6", @@ -2527,7 +2450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473404322827b285cbcd87517f365986bd63af7842c78b2a86ee061715fda61e" dependencies = [ "base64 0.22.1", - "sha2 0.10.9", + "sha2", "stellar-xdr 27.0.0", "thiserror 1.0.69", "wasmparser", @@ -2542,7 +2465,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "sha2 0.10.9", + "sha2", "soroban-spec 21.7.7", "stellar-xdr 21.2.0", "syn 2.0.119", @@ -2558,7 +2481,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "sha2 0.10.9", + "sha2", "soroban-spec 27.0.6", "stellar-xdr 27.0.0", "syn 2.0.119", @@ -2676,7 +2599,7 @@ dependencies = [ "hex", "serde", "serde_with", - "sha2 0.10.9", + "sha2", "stellar-strkey 0.0.13", ] @@ -2833,6 +2756,13 @@ dependencies = [ "winnow", ] +[[package]] +name = "transfer-restriction" +version = "0.1.0" +dependencies = [ + "soroban-sdk 27.0.6", +] + [[package]] name = "treasury-management" version = "0.1.0" @@ -2899,9 +2829,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.129" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2912,9 +2842,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.129" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2922,22 +2852,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.129" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.129" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] diff --git a/contracts/invoice-escrow/src/events.rs b/contracts/invoice-escrow/src/events.rs index 2b9536c5..f6966cf6 100644 --- a/contracts/invoice-escrow/src/events.rs +++ b/contracts/invoice-escrow/src/events.rs @@ -176,6 +176,27 @@ pub fn early_settlement_applied( ); } +/// Investment made event (#518): emitted when an investor locks funds into +/// an invoice's escrow via `invest`. `investment_topped_up` below covers a +/// separate, legacy top-up path; this is the primary event for the +/// multi-investor `FundingInvoice` flow. +pub fn investment_made( + env: &Env, + investor: &Address, + invoice_id: BytesN<32>, + amount: i128, + new_total_position: i128, +) { + env.events().publish( + ( + Symbol::new(env, "investment_made"), + investor.clone(), + invoice_id.clone(), + ), + (amount, new_total_position), + ); +} + /// Investment topped up event. pub fn investment_topped_up( env: &Env, diff --git a/contracts/invoice-escrow/src/lib.rs b/contracts/invoice-escrow/src/lib.rs index 50d4d026..c7a2bf12 100644 --- a/contracts/invoice-escrow/src/lib.rs +++ b/contracts/invoice-escrow/src/lib.rs @@ -1751,6 +1751,7 @@ impl InvoiceEscrow { min_investment, per_investor_cap, token, + None, ) } @@ -1767,6 +1768,7 @@ impl InvoiceEscrow { min_investment: i128, per_investor_cap: Option, token: Address, + fraction_token: Option
, ) -> Result<(), Error> { seller.require_auth(); Self::create_invoice_core( @@ -1780,6 +1782,7 @@ impl InvoiceEscrow { min_investment, per_investor_cap, token, + fraction_token, ) } @@ -1795,6 +1798,7 @@ impl InvoiceEscrow { min_investment: i128, per_investor_cap: Option, token: Address, + fraction_token: Option
, ) -> Result<(), Error> { if funding_target <= 0 { return Err(Error::InvalidAmount); @@ -1827,6 +1831,7 @@ impl InvoiceEscrow { min_funding_threshold, max_funding_threshold, threshold_reached: false, + fraction_token, }; storage::set_invoice(env, invoice_id.clone(), &invoice); events::funding_thresholds_set( @@ -2190,6 +2195,7 @@ impl InvoiceEscrow { // be paid out of other investors' principal or issuer collateral. storage::set_investor_position(&env, &invoice_id, &investor, new_pos); investor_cap::sync_holder(&env, &invoice_id, &investor); + let fraction_token = invoice.fraction_token.clone(); invoice.total_raised = new_total; storage::set_invoice(&env, invoice_id.clone(), &invoice); if returned > 0 { @@ -2202,6 +2208,27 @@ impl InvoiceEscrow { returned, ); } + + // Mint fraction tokens to the investor to represent their + // ownership share (#517), same pattern as the legacy + // create_escrow_with_tokens path's inv_token mint below. Scaled + // to `accepted`, not the originally requested `amount`: any + // `returned` remainder above the funding cap was refunded to + // the investor and must not also get a fraction-token claim. + if let Some(fraction_token) = fraction_token { + env.invoke_contract::<()>( + &fraction_token, + &Symbol::new(&env, "mint"), + soroban_sdk::vec![ + &env, + investor.to_val(), + accepted.into_val(&env), + env.current_contract_address().to_val() + ], + ); + } + + events::investment_made(&env, &investor, invoice_id, accepted, new_pos); return Ok(()); } diff --git a/contracts/invoice-escrow/src/test.rs b/contracts/invoice-escrow/src/test.rs index 79ec1f08..d03558ce 100644 --- a/contracts/invoice-escrow/src/test.rs +++ b/contracts/invoice-escrow/src/test.rs @@ -1,11 +1,13 @@ #![allow(deprecated, unused_variables, dead_code, unused_mut, clippy::all)] use super::*; +use invoice_token::{InvoiceToken, InvoiceTokenClient}; use soroban_sdk::testutils::{Address as _, Events, Ledger}; use soroban_sdk::token::Client as TokenClient; use soroban_sdk::token::StellarAssetClient as AssetClient; use soroban_sdk::{ - contract, contractimpl, Address, BytesN, Env, IntoVal, Symbol, TryFromVal, TryIntoVal, Val, Vec, + contract, contractimpl, Address, BytesN, Env, IntoVal, String as SorobanString, Symbol, + TryFromVal, TryIntoVal, Val, Vec, }; /// Helper function to create a test commitment hash (SHA-256 format) @@ -11029,3 +11031,127 @@ fn issue455_fill_to_target_under_cap_pressure() { EscrowStatus::Funded ); } + +/// `invest` on the multi-investor `FundingInvoice` flow (created via +/// `create_invoice`, distinct from the legacy `register_invoice`/ +/// `EscrowStatus` path) silently emitted no event at all — every other +/// state-changing call in this contract emits one (#518's acceptance +/// criteria: "Events emitted correctly for all state transitions"). Covers +/// the fix: `investment_made` is published with the investor, invoice id, +/// amount, and the investor's new total position. +#[test] +fn test_invest_on_funding_invoice_emits_investment_made_event() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InvoiceEscrow); + let client = InvoiceEscrowClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let investor = Address::generate(&env); + let invoice_id = BytesN::from_array(&env, &[200u8; 32]); + + let token_admin = Address::generate(&env); + let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token = TokenClient::new(&env, &token_id.address()); + let token_asset = AssetClient::new(&env, &token_id.address()); + token_asset.mint(&investor, &100_000i128); + + client.initialize(&admin, &300); + client.create_invoice_legacy( + &invoice_id, + &seller, + &100_000i128, + &1_000u32, + &1_000i128, + &None, + &token_id.address(), + &None, + ); + + client.invest(&invoice_id, &investor, &40_000i128); + + assert_eq!(token.balance(&investor), 60_000i128); + + let events = env.events().all(); + let event = events + .events() + .iter() + .rev() + .find(|e| { + let (_, topics, _) = parse_event(&env, e); + topics + .get(0) + .map(|t| { + Symbol::try_from_val(&env, &t).unwrap() == Symbol::new(&env, "investment_made") + }) + .unwrap_or(false) + }) + .expect("expected investment_made event"); + + let (_contract_addr, topics, data) = parse_event(&env, event); + assert_eq!( + topics, + ( + Symbol::new(&env, "investment_made"), + investor.clone(), + invoice_id.clone(), + ) + .into_val(&env) + ); + let event_data: (i128, i128) = data.try_into_val(&env).unwrap(); + assert_eq!(event_data.0, 40_000i128); // amount invested + assert_eq!(event_data.1, 40_000i128); // new total position +} + +/// `invest` on a `FundingInvoice` created with a `fraction_token` set +/// mints the investor that many fraction tokens on the configured +/// invoice-token contract (#517), so their ownership share has an +/// on-chain representation, not just an internal storage entry. +#[test] +fn test_invest_mints_fraction_tokens_when_fraction_token_configured() { + let env = Env::default(); + env.mock_all_auths(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow = InvoiceEscrowClient::new(&env, &escrow_id); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let investor = Address::generate(&env); + let invoice_id = BytesN::from_array(&env, &[201u8; 32]); + + let payment_token_admin = Address::generate(&env); + let payment_token_id = env.register_stellar_asset_contract_v2(payment_token_admin.clone()); + let payment_token_asset = AssetClient::new(&env, &payment_token_id.address()); + payment_token_asset.mint(&investor, &100_000i128); + + let fraction_token_id = env.register_contract(None, InvoiceToken); + let fraction_token = InvoiceTokenClient::new(&env, &fraction_token_id); + fraction_token.initialize( + &admin, + &SorobanString::from_str(&env, "Invoice Fraction"), + &SorobanString::from_str(&env, "IFRAC"), + &7u32, + &Symbol::new(&env, "inv201"), + &escrow_id, // the escrow contract itself is the authorized minter + ); + + escrow.initialize(&admin, &300); + escrow.create_invoice_legacy( + &invoice_id, + &seller, + &100_000i128, + &1_000u32, + &1_000i128, + &None, + &payment_token_id.address(), + &Some(fraction_token_id.clone()), + ); + + escrow.invest(&invoice_id, &investor, &40_000i128); + + assert_eq!(fraction_token.balance(&investor), 40_000i128); + + escrow.invest(&invoice_id, &investor, &10_000i128); + assert_eq!(fraction_token.balance(&investor), 50_000i128); +} diff --git a/contracts/invoice-escrow/src/test_buy_limit.rs b/contracts/invoice-escrow/src/test_buy_limit.rs index dfacee89..4a7f72ee 100644 --- a/contracts/invoice-escrow/src/test_buy_limit.rs +++ b/contracts/invoice-escrow/src/test_buy_limit.rs @@ -29,6 +29,10 @@ fn setup<'a>() -> Market<'a> { per_investor_cap: None, status: InvoiceStatus::Funded, token: Address::generate(&env), + min_funding_threshold: 1_000, + max_funding_threshold: 1_000, + threshold_reached: true, + fraction_token: None, }; storage::set_invoice(&env, invoice_id.clone(), &invoice); }); diff --git a/contracts/invoice-escrow/src/types.rs b/contracts/invoice-escrow/src/types.rs index 88b1d0e9..cbd37365 100644 --- a/contracts/invoice-escrow/src/types.rs +++ b/contracts/invoice-escrow/src/types.rs @@ -319,6 +319,12 @@ pub struct FundingInvoice { /// Whether `ThresholdReached` has already been emitted for this invoice. /// Guarantees the event fires exactly once per invoice. pub threshold_reached: bool, + /// Optional fraction-token contract address (#517): when set, `invest` + /// mints the investor a matching amount of fraction tokens on this + /// contract so their ownership share has an on-chain representation. + /// `None` preserves the pre-#517 behaviour (payment-only, no fraction + /// tokens) for invoices that don't need one. + pub fraction_token: Option, } /// Funding progress view returned by `get_funding_status`. diff --git a/contracts/invoice-escrow/tests/institutional_controls.rs b/contracts/invoice-escrow/tests/institutional_controls.rs index 9ad068c3..275283f0 100644 --- a/contracts/invoice-escrow/tests/institutional_controls.rs +++ b/contracts/invoice-escrow/tests/institutional_controls.rs @@ -483,8 +483,9 @@ fn collateral_breach_blocks_creation_until_a_real_topup_restores_ratio() { Err(Ok(Error::CollateralTopUpRequired)) ); assert_eq!( - f.client - .try_create_invoice_legacy(&next, &f.issuer, &1000, &1000, &0, &None, &f.token), + f.client.try_create_invoice_legacy( + &next, &f.issuer, &1000, &1000, &0, &None, &f.token, &None, + ), Err(Ok(Error::CollateralTopUpRequired)) ); f.client.top_up_collateral(&f.id, &50); diff --git a/contracts/invoice-escrow/tests/thresholds.rs b/contracts/invoice-escrow/tests/thresholds.rs index 11b07d53..146de421 100644 --- a/contracts/invoice-escrow/tests/thresholds.rs +++ b/contracts/invoice-escrow/tests/thresholds.rs @@ -285,8 +285,16 @@ fn test_legacy_create_invoice_defaults_min_and_max_to_target() { let investor = Address::generate(&env); mint(&env, &f.token, &investor, 500_000); - f.client - .create_invoice_legacy(&id, &f.seller, &80_000i128, &1000u32, &0i128, &None, &f.token); + f.client.create_invoice_legacy( + &id, + &f.seller, + &80_000i128, + &1000u32, + &0i128, + &None, + &f.token, + &None, + ); let status = f.client.get_funding_status(&id); assert_eq!(status.min_threshold, 80_000);