diff --git a/SECURITY.md b/SECURITY.md index 69f17c9..5fee763 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -28,17 +28,38 @@ It is intended for auditors and integrators. #### 1. Solver self-reporting -The contract has **no on-chain proof** that a solver actually sent tokens on the -source chain. `fill_intent` trusts the solver to transfer `dst_token` to the -user on Stellar; the economic incentive not to default is the bond slash -(`slash_solver` takes 10 % of the bond permissionlessly once the fill window -expires). +By default the contract has **no on-chain proof** that a solver actually sent +tokens on the source chain. `fill_intent` trusts the solver to transfer +`dst_token` to the user on Stellar; the economic incentive not to default is the +bond slash (`slash_solver` takes 10 % of the bond permissionlessly once the fill +window expires). **Implication:** A solver with a large enough bond can accept an intent, fail to fill, absorb the 10 % slash, and profit if the spread on the source-chain trade is more valuable than 10 % of their bond. The bond size is the primary economic deterrent. +**Optional cryptographic gate (issue #190).** An admin can call +`set_proof_registry` to point `intent_settlement` at a deployed `ProofRegistry` +(Wormhole-backed). A solver may then call +`fill_intent(solver, intent_id, fill_amount, require_proof = true)`, which +cross-checks a verified source-chain deposit record before any token moves: + +- no proof for the intent → `ProofNotFound`; +- proof's Wormhole `src_chain_id` ≠ mapped `intent.src_chain` → `ProofChainMismatch`; +- `proof.src_amount < intent.src_amount` → `ProofAmountInsufficient`; +- registry not configured → `ProofRegistryNotSet`. + +Every failure path reverts before touching state, so the intent stays +`Accepted` and `slash_solver` remains the backstop (see +`docs/129-proof-mismatch-fallback.md`). This is **opt-in and defaults off**: +`require_proof = false` — the value every existing caller passes — reads no +registry and preserves the self-reported-fill behaviour above byte-for-byte, +mirroring how `DstAllowlistEnabled` defaults off. When the gate is used, trust +for that fill becomes *cryptographic (Guardian quorum) + economic (bond)* rather +than economic alone; residual risk is Guardian collusion, shared with all +Wormhole users. + #### 2. Admin key custody A single `Admin` address controls: @@ -179,8 +200,12 @@ unilaterally by the admin without other protocol preconditions being met first ### Known Limitations -- **No cross-chain proof.** The contract cannot verify the source-chain - transaction. Trust is economic, not cryptographic. +- **Cross-chain proof is opt-in.** By default the contract does not verify the + source-chain transaction and trust is economic, not cryptographic. Issue #190 + adds an admin-enabled `ProofRegistry` gate (`fill_intent(..., require_proof = + true)`) that makes a verified Wormhole deposit a precondition for the fill; + until an operator configures it and solvers opt in, the economic-only model + above still applies. - **Single admin key.** There is no multi-sig or timelock on admin operations. Protocol operators should secure the admin key with a hardware wallet or multi-sig wrapper before mainnet. diff --git a/docs/solver-registry-interface.md b/docs/solver-registry-interface.md new file mode 100644 index 0000000..c6e0786 --- /dev/null +++ b/docs/solver-registry-interface.md @@ -0,0 +1,163 @@ +# Solver Registry — Public ABI + +**Issue:** [#186](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/186) +**Contract:** `solver_registry` (`solver_registry/src/lib.rs`) +**Design:** [`solver-registry-design.md`](./solver-registry-design.md) +**Status:** Implemented — read interface frozen for downstream callers. + +This document is the stable contract surface `intent_settlement` (and off-chain +solver bots) may depend on. The **read** side — `get_tier`, `tier_for`, +`get_tier_table`, the perk getters, `get_reputation_score` — will not change +shape without a new major version. The **write** side (`record_fill` / +`record_failure` / `slash`) is stable but not yet consumed: wiring +`accept_intent` / `slash_solver` to call it is a separate follow-up (design §4, +Option A). + +--- + +## 1. Tier model + +Tier is `min(bond gate, score gate)` — a solver holds tier `t` only if **both** +`bond_amount ≥ threshold[t].min_bond` **and** `score ≥ threshold[t].min_score_bps`. +Score is the 0–10 000 bps reputation value (§3). + +Default table (design §3; `min_bond` shown in USDC, stored in 7-decimal +smallest units): + +| Tier | Name | `min_bond` | `min_score_bps` | `fill_window_bonus_pct` | `slash_bps` | `fee_rebate_bps` | +|-----:|----------|-----------:|----------------:|------------------------:|------------:|-----------------:| +| 0 | Unranked | 50 | 0 | 0 | 1 000 | 0 | +| 1 | Bronze | 500 | 1 000 | 10 | 1 000 | 0 | +| 2 | Silver | 2 000 | 3 500 | 20 | 800 | 0 | +| 3 | Gold | 10 000 | 7 000 | 30 | 600 | 0 | +| 4 | Platinum | 50 000 | 9 000 | 50 | 500 | 0 | + +`min_bond` / `min_score_bps` are **tunable** by the admin within bounds (§2.4). +`fill_window_bonus_pct` / `slash_bps` / `fee_rebate_bps` are **fixed** in the +contract. `fee_rebate_bps` is a reserved slot (design §8, pending tokenomics) +and currently returns 0 for every tier. + +--- + +## 2. Entry points + +### 2.1 Lifecycle / admin + +| Function | Auth | Notes | +|---|---|---| +| `initialize(admin, bond_token, fee_recipient)` | `admin` | Once. Seeds the default tier table. | +| `set_writer(writer)` | admin | Address allowed to drive the write path (§2.3). | +| `set_tier_threshold(tier, min_bond, min_score_bps)` | admin | `tier ∈ 1..=4`; see 2.4. | + +### 2.2 Solver self-service + +| Function | Auth | Notes | +|---|---|---| +| `register_solver(solver, bond_amount)` | `solver` | `bond_amount ≥ tier-0 floor`; pulls bond token. | +| `stake(solver, amount)` | `solver` | Top up bond. | +| `unstake(solver, amount)` | `solver` | Remaining bond must stay ≥ tier-0 floor. | +| `deregister_solver(solver)` | `solver` | Returns full bond, deletes the record. | + +### 2.3 Settlement write path — `caller` must be the configured writer or the admin + +| Function | Returns | Effect | +|---|---|---| +| `record_fill(caller, solver, amount)` | — | `fills_completed += 1`, `total_volume += amount`. | +| `record_failure(caller, solver)` | — | `fills_failed += 1` (no bond movement). | +| `slash(caller, solver)` | `(slash_amount: i128, new_tier: u32)` | Takes `bond * slash_bps(tier) / 10_000` (min 1), transfers it to the fee recipient, `fills_failed += 1`. | + +`caller` is explicit (mirrors `intent_settlement::pause`) so the registry can +accept calls from either the admin or the settlement contract without an +implicit-signer ambiguity. + +### 2.4 `set_tier_threshold` bounds + +- `tier ∈ 1..=4` — tier 0 is the fixed entry floor (`InvalidTier` otherwise). +- `1 ≤ min_bond ≤ 1_000_000 USDC` (`ThresholdOutOfBounds`). +- `min_score_bps ≤ 9_999` (`ThresholdOutOfBounds`) — 10 000 is unreachable by construction. +- The row must stay **strictly** greater than tier `tier-1` and strictly less + than tier `tier+1` on **both** axes (`ThresholdsNotMonotonic`). + +--- + +## 3. Views (stable) + +| Function | Returns | +|---|---| +| `get_tier(solver)` | `u32` 0–4 (unknown solver → 0). | +| `tier_for(score_bps, bond_amount)` | `u32` — pure lookup against the current thresholds; for off-chain pricing. | +| `get_tier_table()` | `Vec` — 5 rows, effective thresholds + fixed perks. | +| `get_fill_window_bonus_pct(tier)` / `get_slash_bps(tier)` / `get_fee_rebate_bps(tier)` | `u32` (`InvalidTier` for `tier ≥ 5`). | +| `get_reputation_score(solver)` | `Option` bps. | +| `compute_reputation_score(record)` | `u32` bps — pure. | +| `get_solver(solver)` | `Option`. | +| `get_solver_count()` | `u32`. | +| `get_writer()` / `get_admin()` / `get_bond_token()` | `Option
`. | + +### `SolverRecord` + +```rust +struct SolverRecord { + address: Address, + bond_amount: i128, // staked bond, smallest unit + fills_completed: u32, + fills_failed: u32, + total_volume: i128, // dst-token units, cumulative + registered_at: u64, + last_slash_time: u64, // 0 = never + slashed_total: i128, // lifetime bond taken by slashing +} +``` + +--- + +## 4. Reputation formula (shared with `intent_settlement`) + +Ported **byte-for-byte** from `intent_settlement::compute_reputation_score`. +Integer-only, cannot panic. + +```text +total = fills_completed + fills_failed (total == 0 → score 0) +base = fills_completed * 10_000 / total success rate, 0..10_000 bps +VOLUME_SCALE = 1_000 * 100 * 10_000_000 (= 1e12) +vol = max(total_volume, 0) +decay = VOLUME_SCALE * 10_000 / (VOLUME_SCALE + vol + 1) +mult = 10_000 - decay / 10 9_000..10_000 bps +score = base * mult / 10_000 +``` + +### Shared test vector + +`solver_registry`'s `score_test_vector` test pins these; the same inputs must +yield the same outputs in `intent_settlement`: + +| `fills_completed` | `fills_failed` | `total_volume` | `score` | +|---:|---:|---:|---:| +| 0 | 0 | 0 | 0 | +| 0 | 5 | 0 | 0 | +| 1 | 0 | 0 | 9 001 | +| 8 | 2 | 0 | 7 200 | +| 8 | 2 | 100 000 000 USDC | 8 000 | + +--- + +## 5. Errors + +| Code | Variant | Trigger | +|---:|---|---| +| 1 | `AlreadyInitialized` | second `initialize` | +| 2 | `NotInitialized` | call before `initialize` | +| 3 | `Unauthorized` | write-path `caller` is neither writer nor admin | +| 4 | `SolverNotRegistered` | unknown solver on a mutating call | +| 5 | `SolverAlreadyRegistered` | `register_solver` for an existing solver | +| 6 | `BondBelowFloor` | register/unstake would leave bond < tier-0 floor | +| 7 | `ZeroAmount` | non-positive amount where positive required | +| 8 | `InsufficientBond` | `unstake` amount > staked bond | +| 9 | `InvalidTier` | tier index ∉ 0..=4, or tier 0 to `set_tier_threshold` | +| 10 | `ThresholdOutOfBounds` | threshold value outside its bound | +| 11 | `ThresholdsNotMonotonic` | thresholds not strictly increasing | +| 12 | `WriterNotSet` | write path used before `set_writer` by a non-admin caller | + +--- + +*Closes #186 (interface documentation).* diff --git a/intent_settlement/Cargo.lock b/intent_settlement/Cargo.lock index 2376e91..ba9cceb 100644 --- a/intent_settlement/Cargo.lock +++ b/intent_settlement/Cargo.lock @@ -86,6 +86,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "block-buffer" version = "0.10.4" @@ -189,7 +195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -395,7 +401,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -420,7 +426,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -450,7 +456,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -520,6 +526,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.32.3" @@ -533,7 +551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -849,6 +867,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "quote" version = "1.0.45" @@ -858,6 +891,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.6" @@ -865,8 +904,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -876,7 +925,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -885,7 +944,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", ] [[package]] @@ -908,6 +985,12 @@ dependencies = [ "syn", ] +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "rfc6979" version = "0.4.0" @@ -1091,7 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1159,7 +1242,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom", + "getrandom 0.2.17", "hex-literal", "hmac", "k256", @@ -1167,8 +1250,8 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand", - "rand_chacha", + "rand 0.8.6", + "rand_chacha 0.3.1", "sec1", "sha2", "sha3", @@ -1220,7 +1303,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand", + "rand 0.8.6", "rustc_version", "serde", "serde_json", @@ -1435,6 +1518,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1450,6 +1539,15 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vortex-intent-settlement" version = "0.1.0" +dependencies = [ + "proptest", + "soroban-sdk", + "vortex-proof-registry", +] + +[[package]] +name = "vortex-proof-registry" +version = "0.1.0" dependencies = [ "soroban-sdk", ] @@ -1460,6 +1558,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.125" @@ -1601,6 +1708,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zerocopy" version = "0.8.52" diff --git a/intent_settlement/Cargo.toml b/intent_settlement/Cargo.toml index 9b7e3a2..b51d323 100644 --- a/intent_settlement/Cargo.toml +++ b/intent_settlement/Cargo.toml @@ -18,10 +18,15 @@ codegen-units = 1 [dependencies] soroban-sdk = { version = "21.0.0" } +# Issue #190: proof-gated fills call into the ProofRegistry contract via its +# generated client. Path dependency; proof_registry also exposes an `rlib` +# crate-type so it can be linked here as a library. +vortex-proof-registry = { path = "../proof_registry" } [dev-dependencies] soroban-sdk = { version = "21.0.0", features = ["testutils"] } proptest = { version = "1.5.0", default-features = false, features = ["std"] } +vortex-proof-registry = { path = "../proof_registry", features = ["testutils"] } [features] testutils = ["soroban-sdk/testutils"] diff --git a/intent_settlement/src/lib.rs b/intent_settlement/src/lib.rs index 6915565..1177e24 100644 --- a/intent_settlement/src/lib.rs +++ b/intent_settlement/src/lib.rs @@ -11,6 +11,10 @@ use soroban_sdk::{ Address, Bytes, BytesN, Env, String, Symbol, Vec, }; +/// Cross-contract client for the `ProofRegistry` contract (issue #190). +/// Used only on the `fill_intent(..., require_proof = true)` path. +use vortex_proof_registry::ProofRegistryClient; + #[cfg(test)] mod test; @@ -144,6 +148,14 @@ pub enum DataKey { /// unpause access) -- resuming the protocol always needs the full /// admin's judgment. Pauser, + + /// **Instance storage.** `Address` of the deployed `ProofRegistry` + /// contract (issue #190, docs/124 §4.1). Absent until the admin calls + /// `set_proof_registry`. Only read by `fill_intent` when it is invoked + /// with `require_proof = true`; a `require_proof = false` fill never + /// touches this key, so proof-gating is fully opt-in and defaults off + /// exactly like `DstAllowlistEnabled`. + ProofRegistry, } // ─── Data Structs ───────────────────────────────────────────────────────────── @@ -408,6 +420,36 @@ pub enum Error { /// If `src_chain` is unknown this error is never raised — unknown chains /// bypass token-format validation so the allowlist remains the sole gate. InvalidSrcToken = 28, + + // ── Proof-gated fills (issue #190, docs/129-proof-mismatch-fallback.md) ── + // + // docs/129 §6 assigns these the logical codes 24–27, but 24 is already + // taken in this enum (`InvalidTokenInterface`) and 22/23 currently carry + // duplicate discriminants from earlier merges. To avoid making that worse + // these use a fresh contiguous block; the doc's semantic mapping is noted + // on each variant. + /// docs/129 §2.4 (code 24). `fill_intent` was called with + /// `require_proof = true` but no `DataKey::ProofRegistry` has been set by + /// the admin. Configuration error — no slash, intent stays `Accepted`. + ProofRegistryNotSet = 30, + /// docs/129 §2.3 (code 25). `ProofRegistry.get_proof(intent_id)` returned + /// `None`: no verified source-chain deposit for this intent yet. Intent + /// stays `Accepted`; the solver should retry once the VAA is relayed, or + /// be slashed if the fill window elapses first. + ProofNotFound = 31, + /// docs/129 §2.2 (code 26). The proof's `src_chain_id` does not equal the + /// Wormhole chain ID mapped from `intent.src_chain`. Hard reject; intent + /// stays `Accepted`. + ProofChainMismatch = 32, + /// docs/129 §2.1 (code 27). `proof.src_amount < intent.src_amount` — the + /// source deposit was smaller than the intent requires. Hard reject; + /// intent stays `Accepted` so the solver can supply a corrected VAA or be + /// slashed at deadline. + ProofAmountInsufficient = 33, + /// docs/129 §4. `intent.src_chain` is not in the canonical + /// chain-name → Wormhole-chain-ID table, so the proof's chain cannot be + /// validated against it. + SrcChainNotSupported = 34, } // ─── Contract ───────────────────────────────────────────────────────────────── @@ -893,6 +935,31 @@ impl IntentSettlement { env.storage().instance().get(&DataKey::Pauser) } + // ── Proof Registry (issue #190) ────────────────────────────────────────── + + /// Admin-only: point this contract at a deployed `ProofRegistry` + /// (docs/124 §4.1). Required before any solver can call + /// `fill_intent(..., require_proof = true)`. Until this is set, proof-gated + /// fills panic with `ProofRegistryNotSet`; ungated fills + /// (`require_proof = false`) are unaffected. + /// + /// Calling again rotates the address (e.g. after a registry upgrade). + pub fn set_proof_registry(env: Env, registry: Address) { + Self::require_admin(&env); + env.storage() + .instance() + .set(&DataKey::ProofRegistry, ®istry); + Self::bump_instance_ttl(&env); + env.events() + .publish((Symbol::new(&env, "proof_registry_set"),), registry); + } + + /// The configured `ProofRegistry` address, or `None` if proof-gating has + /// not been enabled by the admin. + pub fn get_proof_registry(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::ProofRegistry) + } + /// Admin- or pauser-only: halt new intent submission, acceptance, and /// fills for incident response. slash_solver stays permissionless /// throughout, so a solver already holding an Accepted intent can't @@ -1417,7 +1484,40 @@ impl IntentSettlement { /// /// The protocol fee is taken on each individual fill so the fee accounting /// stays consistent regardless of how many fills it takes. - pub fn fill_intent(env: Env, solver: Address, intent_id: BytesN<32>, fill_amount: i128) { + /// + /// ## Proof gating (issue #190, docs/124 §4.2, docs/129) + /// + /// When `require_proof == true`, the fill is cross-checked against a + /// `ProofRegistry` record for `intent_id` before any tokens move: + /// + /// * no `ProofRegistry` configured → `ProofRegistryNotSet`; + /// * no proof for this intent → `ProofNotFound` (docs/129 §2.3); + /// * `proof.src_chain_id` ≠ mapped `intent.src_chain` → `ProofChainMismatch` + /// (docs/129 §2.2); + /// * `proof.src_amount < intent.src_amount` → `ProofAmountInsufficient` + /// (docs/129 §2.1). + /// + /// Every rejection is a `panic_with_error!` before any storage write or + /// transfer, so the intent stays `Accepted`, the fill window keeps running, + /// and `slash_solver` remains callable if the solver never produces a valid + /// proof — exactly the state machine in docs/129 §3. + /// + /// The check is against the immutable `intent.src_amount` (the whole-intent + /// source deposit), not a per-fill quantity, so partial fills each simply + /// re-assert the same condition against the same proof record — no + /// cumulative accounting is involved. + /// + /// `require_proof == false` is 100% backward compatible: the + /// `ProofRegistry` is never read and behaviour is byte-for-byte identical + /// to the pre-#190 contract, mirroring how `DstAllowlistEnabled` defaults + /// off. + pub fn fill_intent( + env: Env, + solver: Address, + intent_id: BytesN<32>, + fill_amount: i128, + require_proof: bool, + ) { // Auth audit: require_auth() is correct. The solver must sign to // authorise the token transfer from their address to the user and fee // recipient. This is the highest-value call site: the solver authorises @@ -1458,6 +1558,13 @@ impl IntentSettlement { panic_with_error!(&env, Error::ZeroAmount); } + // ── Proof gate (issue #190) ───────────────────────────────────────── + // Runs before any token transfer or storage write. Every failure path + // leaves the intent untouched in `Accepted` (docs/129 §3). + if require_proof { + Self::validate_proof(&env, &intent, &intent_id); + } + // Deliver this fill's tokens to the user. let dst_client = token::Client::new(&env, &intent.dst_token); dst_client.transfer(&solver, &intent.user, &fill_amount); @@ -2223,6 +2330,61 @@ impl IntentSettlement { // Unknown chain: skip validation — forward-compatible with future chains. } + // ── Proof gating (issue #190) ──────────────────────────────────────────── + + /// Cross-check an `Accepted` intent against its `ProofRegistry` record. + /// Called from `fill_intent` only when `require_proof == true`. Panics — + /// leaving the intent untouched — on any of the four docs/129 failure + /// modes; returns normally when the proof matches. + fn validate_proof(env: &Env, intent: &IntentRecord, intent_id: &BytesN<32>) { + let registry_addr: Address = env + .storage() + .instance() + .get(&DataKey::ProofRegistry) + .unwrap_or_else(|| panic_with_error!(env, Error::ProofRegistryNotSet)); + + // One extra cross-contract read per gated fill (docs/124 §4.3). This is + // the resource-fee cost the issue calls out; it is only paid when the + // caller opts in via `require_proof = true`. + let registry = ProofRegistryClient::new(env, ®istry_addr); + let proof = registry + .get_proof(intent_id) + .unwrap_or_else(|| panic_with_error!(env, Error::ProofNotFound)); + + let want_chain_id = Self::wormhole_chain_id(env, &intent.src_chain); + if proof.src_chain_id != want_chain_id { + panic_with_error!(env, Error::ProofChainMismatch); + } + + if proof.src_amount < intent.src_amount { + panic_with_error!(env, Error::ProofAmountInsufficient); + } + } + + /// Map a canonical `src_chain` name to its Wormhole chain ID + /// (docs/129 §4, kept in sync with docs/132-supported-chains.md). Panics + /// with `SrcChainNotSupported` for any name not in the table. + fn wormhole_chain_id(env: &Env, src_chain: &String) -> u32 { + let len = src_chain.len() as usize; + // Longest supported name ("avalanche") is 9 bytes. + if len == 0 || len > 16 { + panic_with_error!(env, Error::SrcChainNotSupported); + } + let mut buf = [0u8; 16]; + src_chain.copy_into_slice(&mut buf[..len]); + match &buf[..len] { + b"solana" => 1, + b"ethereum" => 2, + b"bsc" => 4, + b"polygon" => 5, + b"avalanche" => 6, + b"arbitrum" => 23, + b"optimism" => 24, + b"base" => 30, + _ => panic_with_error!(env, Error::SrcChainNotSupported), + } + } + fn require_admin(env: &Env) { let admin: Address = env .storage() diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 9ed8561..1f7995b 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -9,6 +9,9 @@ use crate::{ DataKey, Error, IntentSettlement, IntentSettlementClient, IntentState, SolverRecord, FILL_WINDOW, INTENT_EXPIRY, MIN_BOND, ADMIN_TIMELOCK_DELAY, }; +// Issue #190: proof-gated fill tests drive the real ProofRegistry contract and +// inject records through its `mock_set_proof` testutils entry-point. +use vortex_proof_registry::{ProofRecord, ProofRegistry, ProofRegistryClient}; use soroban_sdk::{ testutils::{Address as _, Ledger}, token, Address, BytesN, Env, String, Symbol, @@ -144,6 +147,49 @@ fn cannot_initialize_twice() { assert_eq!(res, Err(Ok(Error::AlreadyInitialized.into()))); } +/// Issue #148: a second `initialize` call must be rejected *and* must not +/// mutate any of the addresses recorded by the first call. +/// +/// `cannot_initialize_twice` above only re-passes the original arguments, so it +/// cannot catch an implementation that accepts the second call and silently +/// overwrites `Admin` / `FeeRecipient` / `BondToken`. Here the second call +/// supplies three brand-new, distinct addresses; we assert it fails with +/// `AlreadyInitialized` and that every stored address still equals the value +/// from the first call. +#[test] +fn initialize_rejects_second_call_and_keeps_original_config() { + let ctx = setup(); + + // Snapshot the state established by `setup()`'s first `initialize`. + let admin_before = ctx.client().get_admin(); + let fee_recipient_before = ctx.client().get_fee_recipient(); + let bond_token_before = ctx.client().get_bond_token(); + assert_eq!(admin_before, Some(ctx.admin.clone())); + assert_eq!(fee_recipient_before, Some(ctx.fee_recipient.clone())); + assert_eq!(bond_token_before, Some(ctx.bond_token.clone())); + + // Attempt a second initialization with entirely different parameters. + let other_admin = Address::generate(&ctx.env); + let other_fee_recipient = Address::generate(&ctx.env); + let other_bond_token = ctx + .env + .register_stellar_asset_contract_v2(other_admin.clone()) + .address(); + assert_ne!(other_admin, ctx.admin); + assert_ne!(other_fee_recipient, ctx.fee_recipient); + assert_ne!(other_bond_token, ctx.bond_token); + + let res = ctx + .client() + .try_initialize(&other_admin, &other_fee_recipient, &other_bond_token); + assert_eq!(res, Err(Ok(Error::AlreadyInitialized.into()))); + + // Nothing was reset: the rejected call had no side effects. + assert_eq!(ctx.client().get_admin(), admin_before); + assert_eq!(ctx.client().get_fee_recipient(), fee_recipient_before); + assert_eq!(ctx.client().get_bond_token(), bond_token_before); +} + // ─── Admin ────────────────────────────────────────────────────────────────────── #[test] @@ -181,7 +227,7 @@ fn admin_can_propose_and_accept_fee_recipient() { c.accept_intent(&ctx.solver, &id); let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - c.fill_intent(&ctx.solver, &id, &FILL); + c.fill_intent(&ctx.solver, &id, &FILL, &false); assert_eq!(ctx.dst().balance(&new_recipient), fee); } @@ -392,7 +438,7 @@ fn pause_blocks_fill_intent() { let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - let res = c.try_fill_intent(&ctx.solver, &id, &FILL); + let res = c.try_fill_intent(&ctx.solver, &id, &FILL, &false); assert_eq!(res, Err(Ok(Error::ContractPaused.into()))); } @@ -444,7 +490,7 @@ fn pause_blocks_submit_accept_fill_but_allows_cancel_and_slash() { let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - let res = c.try_fill_intent(&ctx.solver, &id, &FILL); + let res = c.try_fill_intent(&ctx.solver, &id, &FILL, &false); assert_eq!(res, Err(Ok(Error::ContractPaused.into()))); // Test allowed operations @@ -801,7 +847,7 @@ fn active_intents_counts_multiple_concurrent_accepted_intents() { // Clearing one via fill decrements the counter but doesn't zero it. let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - c.fill_intent(&ctx.solver, &id1, &FILL); + c.fill_intent(&ctx.solver, &id1, &FILL, &false); assert_eq!(c.get_solver(&ctx.solver).unwrap().active_intents, 1); let res = c.try_deregister_solver(&ctx.solver); assert_eq!(res, Err(Ok(Error::SolverHasActiveIntents.into()))); @@ -823,7 +869,7 @@ fn deregister_after_fill_succeeds() { let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - c.fill_intent(&ctx.solver, &id, &FILL); + c.fill_intent(&ctx.solver, &id, &FILL, &false); // Obligation cleared on fill, so deregistration now succeeds. c.deregister_solver(&ctx.solver); @@ -1121,7 +1167,7 @@ fn full_lifecycle_submit_accept_fill() { // Fill — fund the solver with the output plus the protocol fee they pay. let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - c.fill_intent(&ctx.solver, &id, &FILL); + c.fill_intent(&ctx.solver, &id, &FILL, &false); let intent = c.get_intent(&id).unwrap(); assert!(intent.state == IntentState::Filled); @@ -1155,7 +1201,7 @@ fn get_stats_reflects_cumulative_totals_across_multiple_fills() { c.accept_intent(&ctx.solver, &id1); let fee1 = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee1)); - c.fill_intent(&ctx.solver, &id1, &FILL); + c.fill_intent(&ctx.solver, &id1, &FILL, &false); let (total_intents, total_volume) = c.get_stats(); assert_eq!(total_intents, 1); @@ -1167,7 +1213,7 @@ fn get_stats_reflects_cumulative_totals_across_multiple_fills() { let fill2 = 200 * 10_000_000; let fee2 = fill2 * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(fill2 + fee2)); - c.fill_intent(&ctx.solver, &id2, &fill2); + c.fill_intent(&ctx.solver, &id2, &fill2, &false); let (total_intents, total_volume) = c.get_stats(); assert_eq!(total_intents, 2); @@ -1266,7 +1312,7 @@ fn fill_zero_amount_fails() { let id = ctx.submit(); ctx.client().accept_intent(&ctx.solver, &id); - let res = ctx.client().try_fill_intent(&ctx.solver, &id, &0); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &0, &false); assert_eq!(res, Err(Ok(Error::ZeroAmount.into()))); } @@ -1279,7 +1325,7 @@ fn fill_after_window_fails() { ctx.pass_time(FILL_WINDOW + 1); ctx.dst_admin().mint(&ctx.solver, &FILL); - let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &false); assert_eq!(res, Err(Ok(Error::FillWindowExpired.into()))); } @@ -1295,7 +1341,7 @@ fn fill_by_wrong_solver_fails() { ctx.client().register_solver(&other, &BOND); ctx.dst_admin().mint(&other, &FILL); - let res = ctx.client().try_fill_intent(&other, &id, &FILL); + let res = ctx.client().try_fill_intent(&other, &id, &FILL, &false); assert_eq!(res, Err(Ok(Error::Unauthorized.into()))); } @@ -1713,7 +1759,7 @@ fn fill_intent_state_committed_before_transfer_and_double_fill_rejected() { ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); // Happy-path fill. - c.fill_intent(&ctx.solver, &id, &FILL); + c.fill_intent(&ctx.solver, &id, &FILL, &false); // 1. Storage reflects Filled and fill_amount is set. let intent = c.get_intent(&id).unwrap(); @@ -1728,7 +1774,7 @@ fn fill_intent_state_committed_before_transfer_and_double_fill_rejected() { // 3. A second fill attempt is rejected before any transfer — this is exactly // what a re-entrant token would hit mid-transfer after the CEI reorder. ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); // give solver funds again - let res = c.try_fill_intent(&ctx.solver, &id, &FILL); + let res = c.try_fill_intent(&ctx.solver, &id, &FILL, &false); assert_eq!(res, Err(Ok(Error::IntentAlreadyFilled.into()))); // User's balance must not have increased — no double-payment. @@ -1885,7 +1931,7 @@ fn two_partial_fills_complete_intent() { let fee1 = half * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(half + fee1)); c.accept_intent(&ctx.solver, &id); - c.fill_intent(&ctx.solver, &id, &half); + c.fill_intent(&ctx.solver, &id, &half, &false); // Intent should now be PartiallyFilled and re-opened (solver reset). let intent = c.get_intent(&id).unwrap(); @@ -1901,7 +1947,7 @@ fn two_partial_fills_complete_intent() { let fee2 = remainder * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(remainder + fee2)); c.accept_intent(&ctx.solver, &id); - c.fill_intent(&ctx.solver, &id, &remainder); + c.fill_intent(&ctx.solver, &id, &remainder, &false); let intent = c.get_intent(&id).unwrap(); assert_eq!(intent.state, IntentState::Filled); @@ -1930,7 +1976,7 @@ fn partial_fill_left_incomplete_past_deadline_can_be_expired() { let fee = partial * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(partial + fee)); c.accept_intent(&ctx.solver, &id); - c.fill_intent(&ctx.solver, &id, &partial); + c.fill_intent(&ctx.solver, &id, &partial, &false); // Intent is PartiallyFilled and re-opened with a fresh INTENT_EXPIRY deadline. assert_eq!( @@ -2086,7 +2132,7 @@ fn get_reputation_score_after_fill_is_nonzero() { c.accept_intent(&ctx.solver, &id); let fee = FILL * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); - c.fill_intent(&ctx.solver, &id, &FILL); + c.fill_intent(&ctx.solver, &id, &FILL, &false); let score = c.get_reputation_score(&ctx.solver).unwrap(); assert!(score > 0, "score after fill should be > 0"); @@ -2099,7 +2145,7 @@ fn get_reputation_score_after_fill_is_nonzero() { // rolls back on panic_with_error, so the user's balance stays zero). ctx.dst_admin().mint(&ctx.solver, &overflow_fill); - let res = c.try_fill_intent(&ctx.solver, &id, &overflow_fill); + let res = c.try_fill_intent(&ctx.solver, &id, &overflow_fill, &false); assert_eq!(res, Err(Ok(Error::FeeOverflow.into()))); } @@ -2118,7 +2164,7 @@ fn fill_intent_fee_at_boundary_does_not_overflow() { ctx.dst_admin().mint(&ctx.solver, &(boundary_fill + fee)); // Should succeed (no overflow). - c.fill_intent(&ctx.solver, &id, &boundary_fill); + c.fill_intent(&ctx.solver, &id, &boundary_fill, &false); assert!(c.get_intent(&id).unwrap().state == IntentState::Filled); } @@ -2434,7 +2480,7 @@ fn unpause_restores_solver_bond_management() { let fee = MIN_DST * 5 / 10_000; ctx.dst_admin().mint(&ctx.solver, &(MIN_DST + fee)); c.accept_intent(&ctx.solver, &id); - c.fill_intent(&ctx.solver, &id, &MIN_DST); + c.fill_intent(&ctx.solver, &id, &MIN_DST, &false); let intent = c.get_intent(&id).unwrap(); assert_eq!(intent.state, IntentState::Filled); @@ -2871,3 +2917,140 @@ fn unknown_chain_bypasses_token_format_validation() { &deadline, ); } + +// ─── Proof-gated fills (issue #190, docs/129-proof-mismatch-fallback.md) ───── + +/// Deploy a `ProofRegistry`, initialise it, and point the settlement contract +/// at it via `set_proof_registry`. Returns the registry address. +fn deploy_proof_registry(ctx: &Ctx) -> Address { + // The Wormhole Core address is irrelevant here — these tests inject proofs + // through `mock_set_proof`, which never touches the verification path. + let wormhole_core = Address::generate(&ctx.env); + let reg_id = ctx.env.register_contract(None, ProofRegistry); + ProofRegistryClient::new(&ctx.env, ®_id).initialize(&ctx.admin, &wormhole_core); + ctx.client().set_proof_registry(®_id); + reg_id +} + +/// Inject a proof record for `intent_id` into the registry at `reg_id`. +fn set_proof(ctx: &Ctx, reg_id: &Address, intent_id: &BytesN<32>, src_chain_id: u32, src_amount: i128) { + ProofRegistryClient::new(&ctx.env, reg_id).mock_set_proof(&ProofRecord { + intent_id: intent_id.clone(), + src_user: String::from_str(&ctx.env, "0x0000000000000000000000000000000000000000"), + src_chain_id, + src_token: String::from_str(&ctx.env, "0x0000000000000000000000000000000000000000"), + src_amount, + vaa_sequence: 1, + received_at: 0, + }); +} + +/// Register the solver, submit a standard `"ethereum"` intent (Wormhole chain +/// id 2), accept it, and mint the solver enough dst token to fill + fee. +fn accepted_intent(ctx: &Ctx) -> BytesN<32> { + ctx.register_solver(); + let id = ctx.submit(); + ctx.client().accept_intent(&ctx.solver, &id); + let fee = FILL * 5 / 10_000; + ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); + id +} + +/// `require_proof = false` behaves exactly as before — no registry needed. +#[test] +fn proof_gate_off_leaves_behaviour_unchanged() { + let ctx = setup(); + let id = accepted_intent(&ctx); + ctx.client().fill_intent(&ctx.solver, &id, &FILL, &false); + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Filled); +} + +/// docs/129 §2.4 — `require_proof = true` with no registry configured. +#[test] +fn proof_required_without_registry_is_config_error() { + let ctx = setup(); + let id = accepted_intent(&ctx); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(res, Err(Ok(Error::ProofRegistryNotSet.into()))); + // No slash path triggered — intent is still Accepted. + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Accepted); +} + +/// docs/129 §2.3 — registry configured but no proof for this intent. +#[test] +fn proof_required_but_absent_rejects_fill() { + let ctx = setup(); + let _reg = deploy_proof_registry(&ctx); + let id = accepted_intent(&ctx); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(res, Err(Ok(Error::ProofNotFound.into()))); + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Accepted); +} + +/// docs/129 §2.2 — proof exists but for the wrong source chain. +#[test] +fn proof_chain_mismatch_rejects_fill() { + let ctx = setup(); + let reg = deploy_proof_registry(&ctx); + let id = accepted_intent(&ctx); + // Intent is "ethereum" (chain id 2); proof claims Polygon (5). + set_proof(&ctx, ®, &id, 5, SRC_AMT); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(res, Err(Ok(Error::ProofChainMismatch.into()))); + let intent = ctx.client().get_intent(&id).unwrap(); + assert_eq!(intent.state, IntentState::Accepted); // still slashable + + // slash_solver stays reachable after the mismatch rejection (docs/129 §3). + ctx.pass_time(FILL_WINDOW + 1); + ctx.client().slash_solver(&id); + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Open); +} + +/// docs/129 §2.1 — proof's source deposit is smaller than the intent requires. +#[test] +fn proof_amount_insufficient_rejects_fill() { + let ctx = setup(); + let reg = deploy_proof_registry(&ctx); + let id = accepted_intent(&ctx); + set_proof(&ctx, ®, &id, 2, SRC_AMT - 1); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(res, Err(Ok(Error::ProofAmountInsufficient.into()))); + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Accepted); +} + +/// Happy path — matching chain and sufficient amount → the fill goes through. +#[test] +fn matching_proof_allows_fill() { + let ctx = setup(); + let reg = deploy_proof_registry(&ctx); + let id = accepted_intent(&ctx); + set_proof(&ctx, ®, &id, 2, SRC_AMT); + ctx.client().fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(ctx.client().get_intent(&id).unwrap().state, IntentState::Filled); +} + +/// An `intent.src_chain` outside the docs/129 §4 mapping table cannot be +/// proof-validated. +#[test] +fn unsupported_src_chain_rejects_gated_fill() { + let ctx = setup(); + let reg = deploy_proof_registry(&ctx); + ctx.register_solver(); + // Submit a "cosmos" intent (not in the Wormhole chain-id table). + let deadline: Option = None; + let id = ctx.client().submit_intent( + &ctx.user, + &String::from_str(&ctx.env, "cosmos"), + &String::from_str(&ctx.env, "cosmos1qyqa2zn5c925lyz4gq5qxsrx5gq5qxsr"), + &SRC_AMT, + &ctx.dst_token, + &MIN_DST, + &deadline, + ); + ctx.client().accept_intent(&ctx.solver, &id); + let fee = FILL * 5 / 10_000; + ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); + set_proof(&ctx, ®, &id, 2, SRC_AMT); + let res = ctx.client().try_fill_intent(&ctx.solver, &id, &FILL, &true); + assert_eq!(res, Err(Ok(Error::SrcChainNotSupported.into()))); +} diff --git a/proof_registry/Cargo.lock b/proof_registry/Cargo.lock new file mode 100644 index 0000000..fb62d25 --- /dev/null +++ b/proof_registry/Cargo.lock @@ -0,0 +1,1634 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.2.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 = "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 = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.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 = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.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 = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.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 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.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 = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" +dependencies = [ + "backtrace", + "curve25519-dalek", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6edf92749fd8399b417192d301c11f710b9cdce15789a3d157785ea971576fa" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcdf04484af7cc731a7a48ad1d9f5f940370edeea84734434ceaf398a6b862e" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "derive_arbitrary", + "ed25519-dalek", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" +dependencies = [ + "crate-git-revision", + "darling 0.20.11", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-spec" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dafbde981b141b191c6c036abc86097070ddd6eaaa33b273701449501e43d3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.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 = "vortex-proof-registry" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[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" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/proof_registry/Cargo.toml b/proof_registry/Cargo.toml index 4c58b4a..184a5be 100644 --- a/proof_registry/Cargo.toml +++ b/proof_registry/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2021" publish = false +# `rlib` in addition to `cdylib` so `intent_settlement` can depend on this +# crate as a library (for `ProofRegistryClient`) while `stellar contract build` +# still emits the cdylib wasm (issue #190). [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [profile.release] opt-level = "z" diff --git a/proof_registry/src/lib.rs b/proof_registry/src/lib.rs index fd68b33..3fcac40 100644 --- a/proof_registry/src/lib.rs +++ b/proof_registry/src/lib.rs @@ -1,37 +1,91 @@ #![no_std] -//! Vortex Protocol — Mock Cross-Chain Proof Oracle (`proof_registry`) +//! Vortex Protocol — Cross-Chain Proof Registry (`proof_registry`) //! -//! This crate implements the `ProofRegistry` contract as defined in -//! [`docs/124-proof-verification-interface.md`]. It provides: +//! This crate implements the `ProofRegistry` contract defined in +//! [`docs/124-proof-verification-interface.md`]. It records, per Vortex +//! `intent_id`, a [`ProofRecord`] attesting that the user's source-chain +//! deposit actually happened, so `intent_settlement::fill_intent` can gate a +//! solver's claimed fill on a verified cross-chain message. //! -//! 1. **Production interface** — the same storage layout and public API that -//! a real Wormhole-backed registry will expose, so `intent_settlement` can -//! be written against a stable interface today. +//! ## Verification path (issue #189) //! -//! 2. **Test-controllable back-door** — a `mock_set_proof` entry-point -//! (compiled only when the `testutils` feature is active) that lets tests -//! inject arbitrary `ProofRecord` values directly into storage, bypassing -//! VAA verification. This allows integration tests to exercise -//! proof-gated `fill_intent` paths without a real Wormhole guardian quorum. +//! [`ProofRegistry::receive_message`] performs the **production** verification +//! flow: //! -//! ## Design rationale +//! 1. Hand the raw VAA bytes to the **Wormhole Core contract** (address stored +//! at [`ProofKey::WormholeCore`]) via a cross-contract call. That contract +//! checks the Guardian signature set and returns the decoded VAA envelope +//! ([`VaaEnvelope`]). A malformed VAA or an invalid/for-quorum signature +//! set traps there and reverts this call — an unverified payload is never +//! touched. +//! 2. Enforce the emitter allowlist: the envelope's `emitter_chain` / +//! `emitter_address` (the values the Guardians actually signed over, **not** +//! the application payload) must match [`ProofKey::AuthorizedEmitter`] for +//! that chain, otherwise [`Error::EmitterNotAuthorized`]. +//! 3. Decode the fixed 102-byte application payload and cross-check that its +//! self-declared `src_chain_id` agrees with the signed `emitter_chain` +//! ([`Error::EmitterChainMismatch`]). +//! 4. Reject replays on two independent axes: one proof per `intent_id` +//! ([`Error::ProofAlreadyExists`]) **and** one `(emitter_chain, sequence)` +//! pair ever ([`Error::VaaAlreadyProcessed`]) — a VAA replayed for a +//! different `intent_id` is still caught. +//! 5. Persist the [`ProofRecord`], populating `vaa_sequence` from the real VAA +//! header rather than a placeholder. //! -//! Keeping mock behaviour behind a Cargo feature flag means: -//! - The mock back-door is provably absent from the release WASM binary. -//! - CI tests use the exact same contract ABI as production. -//! - The mock can be replaced by the real implementation by removing the -//! `mock_set_proof` method and wiring up actual VAA verification, with -//! no changes required to `intent_settlement` or its tests. +//! Only the Wormhole Core call boundary is external; every check above is this +//! contract's own logic. +//! +//! ## Test-controllable back-door +//! +//! `mock_set_proof` / `mock_remove_proof` (compiled only under the `testutils` +//! Cargo feature) let integration tests inject arbitrary [`ProofRecord`]s +//! directly into storage. They are a separate entry-point and do not touch — +//! and cannot weaken — the `receive_message` verification path above. A +//! release build compiled without `testutils` does not contain them. Gating +//! them further (so a `testutils` build still can't be abused) is tracked as a +//! separate security issue and is out of scope here. use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Bytes, - BytesN, Env, String, Symbol, + contract, contractclient, contracterror, contractimpl, contracttype, panic_with_error, Address, + Bytes, BytesN, Env, String, Symbol, }; #[cfg(test)] mod test; +// ─── Wormhole Core boundary ────────────────────────────────────────────────── + +/// The decoded, signature-verified VAA envelope returned by the Wormhole Core +/// contract. These are the header fields the Guardian set signs over; the +/// application `payload` is opaque to Wormhole and decoded by this contract. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct VaaEnvelope { + /// Wormhole chain ID of the chain that emitted the message. + pub emitter_chain: u32, + /// Emitter contract address on the source chain, left-padded to 32 bytes. + pub emitter_address: BytesN<32>, + /// Per-emitter monotonic sequence number from the VAA header. + pub sequence: u64, + /// The application payload (for Vortex: the fixed 102-byte deposit record). + pub payload: Bytes, +} + +/// Minimal view of the Wormhole Core contract that `proof_registry` depends on. +/// +/// The real Stellar Wormhole Core deployment exposes an equivalent entry-point +/// that verifies the Guardian signature set and returns the parsed envelope; +/// integration tests substitute a mock at this boundary (and only this +/// boundary). +#[contractclient(name = "WormholeCoreClient")] +pub trait WormholeCore { + /// Verify the Guardian signatures on `vaa` and return its decoded envelope. + /// Must trap if the VAA is malformed or the signature set is invalid / + /// below quorum. + fn parse_and_verify_vaa(env: Env, vaa: Bytes) -> VaaEnvelope; +} + // ─── Storage Keys ───────────────────────────────────────────────────────────── #[contracttype] @@ -39,24 +93,26 @@ mod test; pub enum ProofKey { /// Admin address (set in `initialize`). Admin, - /// Wormhole Core contract address used for VAA verification in production. - /// Stored but not called in this mock — present so the storage layout - /// matches the future production contract. + /// Wormhole Core contract address used for VAA verification. WormholeCore, - /// Authorized emitter address on a given Wormhole source-chain ID. - /// Key: `(chain_id: u16)` → `emitter: BytesN<32>`. - AuthorizedEmitter(u32), // u32 wraps u16 — Soroban contracttype requires u32 + /// Authorized emitter address for a given Wormhole source-chain ID. + /// Key: `chain_id: u32` (wraps a `u16`) → `emitter: BytesN<32>`. + AuthorizedEmitter(u32), /// Verified proof record keyed by Vortex `intent_id`. Proof(BytesN<32>), + /// Replay guard: presence means the VAA with this `(emitter_chain, + /// sequence)` pair has already been processed, regardless of which + /// `intent_id` it carried. + SeenVaa(u32, u64), } // ─── Data Types ─────────────────────────────────────────────────────────────── /// A verified record that a source-chain deposit occurred for `intent_id`. /// -/// In production this is populated by `receive_message` after Guardian -/// signature verification. In the mock it may also be populated by -/// `mock_set_proof` (testutils feature) for test-controlled scenarios. +/// Populated by `receive_message` after the Wormhole Core contract has +/// verified the Guardian signatures. Under the `testutils` feature it may +/// also be injected by `mock_set_proof`. #[contracttype] #[derive(Clone, Debug, PartialEq)] pub struct ProofRecord { @@ -70,7 +126,8 @@ pub struct ProofRecord { pub src_token: String, /// Amount deposited on the source chain in that token's smallest unit. pub src_amount: i128, - /// Wormhole VAA sequence number — used for replay protection. + /// Wormhole VAA sequence number, taken from the verified VAA header — used + /// for replay protection independent of `intent_id`. pub vaa_sequence: u64, /// Ledger timestamp when this proof was registered on Stellar. pub received_at: u64, @@ -86,17 +143,23 @@ pub enum Error { AlreadyInitialized = 1, /// Caller is not the admin. Unauthorized = 2, - /// `receive_message` received a VAA whose emitter is not in the authorized - /// list for the claimed source chain. + /// The VAA envelope's `(emitter_chain, emitter_address)` is not the + /// authorized emitter for that chain. EmitterNotAuthorized = 3, /// A proof for this `intent_id` already exists (replay protection). ProofAlreadyExists = 4, - /// `get_proof` or `has_proof` was called for an `intent_id` with no record. + /// `get_proof` / `has_proof` for an `intent_id` with no record. ProofNotFound = 5, - /// VAA payload could not be decoded (wrong length or malformed). + /// VAA application payload could not be decoded (wrong length or malformed). InvalidPayload = 6, /// Contract not initialized (`Admin` key absent). NotInitialized = 7, + /// The VAA with this `(emitter_chain, sequence)` was already processed — + /// distinct from `ProofAlreadyExists`, which is keyed on `intent_id`. + VaaAlreadyProcessed = 8, + /// The application payload's self-declared `src_chain_id` does not match + /// the `emitter_chain` the Guardians signed over. + EmitterChainMismatch = 9, } // ─── Contract ───────────────────────────────────────────────────────────────── @@ -123,8 +186,8 @@ impl ProofRegistry { // ── Admin ───────────────────────────────────────────────────────────────── - /// Admin-only: register a trusted emitter for a given Wormhole source-chain - /// ID. Only VAAs originating from this emitter on `chain_id` will be + /// Admin-only: register the trusted emitter for a Wormhole source-chain ID. + /// Only VAAs whose **envelope** names this emitter on `chain_id` are /// accepted by `receive_message`. pub fn set_authorized_emitter(env: Env, chain_id: u32, emitter: BytesN<32>) { Self::require_admin(&env); @@ -144,10 +207,8 @@ impl ProofRegistry { env.storage() .instance() .remove(&ProofKey::AuthorizedEmitter(chain_id)); - env.events().publish( - (Symbol::new(&env, "emitter_removed"),), - chain_id, - ); + env.events() + .publish((Symbol::new(&env, "emitter_removed"),), chain_id); } /// Return the authorized emitter for `chain_id`, or `None` if unset. @@ -157,45 +218,75 @@ impl ProofRegistry { .get(&ProofKey::AuthorizedEmitter(chain_id)) } + /// The configured Wormhole Core contract address, or `None` before init. + pub fn get_wormhole_core(env: Env) -> Option
{ + env.storage().instance().get(&ProofKey::WormholeCore) + } + // ── Message Receipt ─────────────────────────────────────────────────────── - /// Receive and verify a Wormhole VAA, then store the decoded proof. - /// - /// **Production behaviour (not yet implemented here):** - /// 1. Call the Wormhole Core contract to verify Guardian signatures on `vaa`. - /// 2. Decode the VAA body: extract `emitter_chain`, `emitter_address`, - /// and the 102-byte payload defined in §3.4 of the design doc. - /// 3. Check `emitter_address` is in `AuthorizedEmitter(emitter_chain)`. - /// 4. Decode the payload into a `ProofRecord`. - /// 5. Reject if a proof for this `intent_id` already exists. - /// 6. Store the `ProofRecord` under `ProofKey::Proof(intent_id)`. - /// - /// **Mock behaviour (this implementation):** - /// VAA verification is skipped. The method parses the raw 102-byte - /// payload directly without checking Guardian signatures or emitter - /// authorization. This is intentional: tests call this with a hand-crafted - /// payload rather than a real signed VAA. + /// Receive a Wormhole VAA, verify it, and store the decoded proof. /// - /// Payload layout (102 bytes, big-endian): - /// ``` - /// [0..32] intent_id (BytesN<32>) - /// [32..52] src_user (20-byte EVM address, zero-padded on Solana) + /// Application payload layout (102 bytes, big-endian), decoded from + /// `envelope.payload` after Guardian verification: + /// ```text + /// [0..32] intent_id (BytesN<32>) + /// [32..52] src_user (20-byte EVM address, zero-padded on Solana) /// [52..54] src_chain_id (u16) - /// [54..86] src_token (32 bytes, address padded) - /// [86..102] src_amount (i128, big-endian) + /// [54..86] src_token (32 bytes, address padded) + /// [86..102] src_amount (i128, big-endian) /// ``` + /// + /// Fails closed: + /// * malformed VAA / bad signature set → traps inside the Wormhole Core + /// call, reverting this call; + /// * `Error::EmitterNotAuthorized` — envelope emitter not on the allowlist; + /// * `Error::EmitterChainMismatch` — payload chain ≠ signed emitter chain; + /// * `Error::InvalidPayload` — payload not exactly 102 bytes; + /// * `Error::ProofAlreadyExists` / `Error::VaaAlreadyProcessed` — replay. pub fn receive_message(env: Env, vaa: Bytes) { - // Payload must be exactly 102 bytes. - if vaa.len() != 102 { + let core: Address = env + .storage() + .instance() + .get(&ProofKey::WormholeCore) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + + // ── Boundary: Guardian signature verification ──────────────────────── + // The Wormhole Core contract checks the signature set and returns the + // decoded envelope. Anything wrong with the VAA traps there. + let envelope = WormholeCoreClient::new(&env, &core).parse_and_verify_vaa(&vaa); + + // ── Emitter authorization (envelope, not payload) ─────────────────── + let authorized: BytesN<32> = env + .storage() + .instance() + .get(&ProofKey::AuthorizedEmitter(envelope.emitter_chain)) + .unwrap_or_else(|| panic_with_error!(&env, Error::EmitterNotAuthorized)); + if authorized != envelope.emitter_address { + panic_with_error!(&env, Error::EmitterNotAuthorized); + } + + // ── Decode the fixed 102-byte application payload ──────────────────── + let payload = envelope.payload; + if payload.len() != 102 { panic_with_error!(&env, Error::InvalidPayload); } - // Decode intent_id (bytes 0..32). - let intent_id: BytesN<32> = vaa.slice(0..32).try_into().unwrap_or_else(|_| { - panic_with_error!(&env, Error::InvalidPayload) - }); + let intent_id: BytesN<32> = payload + .slice(0..32) + .try_into() + .unwrap_or_else(|_| panic_with_error!(&env, Error::InvalidPayload)); + + let src_chain_id: u32 = + ((Self::byte(&env, &payload, 52) as u32) << 8) | (Self::byte(&env, &payload, 53) as u32); - // Reject replays. + // The payload's self-declared chain must agree with what the Guardians + // actually signed over. + if src_chain_id != envelope.emitter_chain { + panic_with_error!(&env, Error::EmitterChainMismatch); + } + + // ── Replay protection (two independent axes) ───────────────────────── if env .storage() .persistent() @@ -203,88 +294,64 @@ impl ProofRegistry { { panic_with_error!(&env, Error::ProofAlreadyExists); } + let seq_key = ProofKey::SeenVaa(envelope.emitter_chain, envelope.sequence); + if env.storage().persistent().has(&seq_key) { + panic_with_error!(&env, Error::VaaAlreadyProcessed); + } - // Decode src_chain_id (bytes 52..54) as big-endian u16 → u32. - let chain_hi = vaa.get(52) as u32; - let chain_lo = vaa.get(53) as u32; - let src_chain_id: u32 = (chain_hi << 8) | chain_lo; - - // In production: check emitter authorization here. - // Mock: skip that check entirely. - - // Decode src_amount (bytes 86..102) as big-endian i128. + // ── Decode remaining fields ───────────────────────────────────────── let mut amount_bytes = [0u8; 16]; let mut idx = 0usize; while idx < 16 { - amount_bytes[idx] = vaa.get((86 + idx) as u32) as u8; + amount_bytes[idx] = Self::byte(&env, &payload, 86 + idx as u32); idx += 1; } let src_amount = i128::from_be_bytes(amount_bytes); - // vaa_sequence is not present in the payload (it lives in the VAA - // envelope, not the application payload). Use 0 for the mock; the - // real implementation will extract it from the VAA header. + let src_user = Self::bytes_to_hex_string(&env, &payload.slice(32..52)); + let src_token = Self::bytes_to_hex_string(&env, &payload.slice(54..86)); let now = env.ledger().timestamp(); - // src_user and src_token are stored as hex strings of the raw bytes - // for simplicity in the mock. Production will use the actual encoding. - let src_user = Self::bytes_to_hex_string(&env, &vaa.slice(32..52)); - let src_token = Self::bytes_to_hex_string(&env, &vaa.slice(54..86)); - let record = ProofRecord { intent_id: intent_id.clone(), src_user, src_chain_id, src_token, src_amount, - vaa_sequence: 0, + vaa_sequence: envelope.sequence, received_at: now, }; env.storage() .persistent() .set(&ProofKey::Proof(intent_id.clone()), &record); + env.storage().persistent().set(&seq_key, &true); env.events().publish( (Symbol::new(&env, "proof_received"),), - (intent_id, src_chain_id, src_amount), + (intent_id, src_chain_id, src_amount, envelope.sequence), ); } // ── Proof Queries ───────────────────────────────────────────────────────── - /// Return the stored `ProofRecord` for `intent_id`, or `None` if not yet - /// received. + /// Return the stored `ProofRecord` for `intent_id`, or `None`. pub fn get_proof(env: Env, intent_id: BytesN<32>) -> Option { - env.storage() - .persistent() - .get(&ProofKey::Proof(intent_id)) + env.storage().persistent().get(&ProofKey::Proof(intent_id)) } - /// Returns `true` iff a valid proof exists for `intent_id`. + /// Returns `true` iff a verified proof exists for `intent_id`. pub fn has_proof(env: Env, intent_id: BytesN<32>) -> bool { - env.storage() - .persistent() - .has(&ProofKey::Proof(intent_id)) + env.storage().persistent().has(&ProofKey::Proof(intent_id)) } // ── Test Back-Door ──────────────────────────────────────────────────────── - /// **Test-only** (available only when the `testutils` Cargo feature is - /// enabled): directly insert a `ProofRecord` into storage, bypassing - /// all VAA parsing and Guardian verification. - /// - /// This allows integration tests to set up any proof scenario — including - /// edge cases like proofs with mismatched amounts or chain IDs — without - /// constructing a real signed VAA. - /// - /// The method is intentionally not guarded by admin auth in the mock so - /// that any test address can call it. A production implementation would - /// not expose this method at all. + /// **Test-only** (`testutils` feature): insert a `ProofRecord` directly, + /// bypassing all VAA parsing and Guardian verification. Separate from the + /// `receive_message` path — see the module doc. #[cfg(feature = "testutils")] pub fn mock_set_proof(env: Env, record: ProofRecord) { - // Reject replays (same as receive_message) so tests that accidentally - // call this twice get a clear error rather than silent overwrite. if env .storage() .persistent() @@ -297,8 +364,7 @@ impl ProofRegistry { .set(&ProofKey::Proof(record.intent_id.clone()), &record); } - /// **Test-only**: remove a stored proof. Useful for testing the - /// "proof not found" path after a proof was previously inserted. + /// **Test-only** (`testutils` feature): remove a stored proof. #[cfg(feature = "testutils")] pub fn mock_remove_proof(env: Env, intent_id: BytesN<32>) { env.storage() @@ -317,24 +383,37 @@ impl ProofRegistry { admin.require_auth(); } - /// Convert a raw byte slice into a lowercase hex `String`. - /// Used to represent `src_user` and `src_token` fields decoded from the - /// VAA payload without pulling in an external base58/hex crate. + /// Read one byte of `bytes`, failing closed with `InvalidPayload` if the + /// index is out of range (callers have already length-checked, so this is + /// defence-in-depth rather than an expected path). + fn byte(env: &Env, bytes: &Bytes, i: u32) -> u8 { + bytes + .get(i) + .unwrap_or_else(|| panic_with_error!(env, Error::InvalidPayload)) + } + + /// Convert a raw byte slice into a lowercase `0x`-prefixed hex `String`. + /// Callers pass the 20-byte `src_user` and 32-byte `src_token` slices, so + /// the output is at most `2 + 64 = 66` characters; anything longer is a + /// malformed payload and fails closed. fn bytes_to_hex_string(env: &Env, bytes: &Bytes) -> String { const HEX: &[u8] = b"0123456789abcdef"; - // Each byte becomes two hex characters; prepend "0x". let len = bytes.len(); - // Build into a fixed-size Bytes then convert to String. - let mut out = Bytes::new(env); - out.push_back(b'0'); - out.push_back(b'x'); + if len > 32 { + panic_with_error!(env, Error::InvalidPayload); + } + let mut buf = [0u8; 66]; + buf[0] = b'0'; + buf[1] = b'x'; + let mut w = 2usize; let mut i = 0u32; while i < len { - let byte = bytes.get(i) as u8; - out.push_back(HEX[(byte >> 4) as usize]); - out.push_back(HEX[(byte & 0x0f) as usize]); + let byte = bytes.get(i).unwrap_or(0); + buf[w] = HEX[(byte >> 4) as usize]; + buf[w + 1] = HEX[(byte & 0x0f) as usize]; + w += 2; i += 1; } - String::from_bytes(env, &out) + String::from_bytes(env, &buf[..w]) } } diff --git a/proof_registry/src/test.rs b/proof_registry/src/test.rs index 3d16eb4..5649d04 100644 --- a/proof_registry/src/test.rs +++ b/proof_registry/src/test.rs @@ -1,31 +1,105 @@ #![cfg(test)] -//! Integration tests for the `ProofRegistry` mock oracle. +//! Tests for the `ProofRegistry` contract. //! -//! These tests cover: +//! The Wormhole Core contract is the **only** mocked boundary: [`MockWormholeCore`] +//! stands in for Guardian-quorum signature verification. Everything downstream +//! of it — emitter authorization, payload decoding, chain cross-checks and +//! replay protection — is the real `proof_registry` logic under test. //! -//! * The `mock_set_proof` / `mock_remove_proof` test back-doors. -//! * `receive_message` with a hand-crafted 102-byte payload. -//! * Replay protection (duplicate intent_id rejected). -//! * `has_proof` / `get_proof` query semantics. -//! * Authorized emitter management (set / remove / query). -//! * Invalid payload length rejection. -//! -//! The `testutils` feature must be enabled for the mock back-door methods to -//! be available. This file is only compiled under `#[cfg(test)]`. - -use crate::{Error, ProofRecord, ProofRegistry, ProofRegistryClient}; +//! Fixtures for issue #189: +//! * `receive_message_stores_verified_proof` — a valid VAA. +//! * `receive_message_rejects_unauthorized_emitter` — VAA from an emitter not +//! on the allowlist. +//! * `receive_message_rejects_tampered_payload` — VAA whose payload was +//! altered after signing (Core verification traps). +//! * `receive_message_rejects_replayed_sequence` — a VAA whose +//! `(emitter_chain, sequence)` was already processed. + +use crate::{Error, ProofRecord, ProofRegistry, ProofRegistryClient, VaaEnvelope}; use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Bytes, BytesN, Env, String, + contract, contractimpl, testutils::Address as _, Address, Bytes, BytesN, Env, String, }; +// ─── Mock Wormhole Core (the one mocked boundary) ──────────────────────────── + +/// Test double for the Wormhole Core contract. +/// +/// It parses the real Wormhole VAA wire layout and, in place of a secp256k1 +/// Guardian-quorum check, requires that signature 0's `r` field equals +/// `sha256(body)`. This gives a faithful "the signature commits to the whole +/// body" property — any post-signing tamper of the envelope or payload makes +/// verification trap — without needing a Guardian set in-process. +/// +/// VAA layout it understands: +/// ```text +/// [0] version (== 1) +/// [1..5] guardian_set_index (u32 BE, ignored) +/// [5] signature_count n (>= 1 required = quorum stand-in) +/// [6 .. 6+66n] signatures; sig 0 = [idx u8][r 32][s 32][v u8] +/// --- body (offset B = 6 + 66n) --- +/// [B+0..B+4] timestamp u32 BE (ignored) +/// [B+4..B+8] nonce u32 BE (ignored) +/// [B+8..B+10] emitter_chain u16 BE +/// [B+10..B+42] emitter_address 32 bytes +/// [B+42..B+50] sequence u64 BE +/// [B+50] consistency_level u8 (ignored) +/// [B+51..] payload +/// ``` +#[contract] +pub struct MockWormholeCore; + +#[contractimpl] +impl MockWormholeCore { + pub fn parse_and_verify_vaa(env: Env, vaa: Bytes) -> VaaEnvelope { + assert!(vaa.len() >= 6, "VAA header truncated"); + assert_eq!(vaa.get(0).unwrap(), 1, "unsupported VAA version"); + + let n = vaa.get(5).unwrap() as u32; + assert!(n >= 1, "no Guardian signatures / quorum not met"); + + let body_start = 6 + 66 * n; + assert!(vaa.len() as u32 > body_start, "VAA body missing"); + let body = vaa.slice(body_start..vaa.len()); + + // Guardian-signature stand-in: sig 0's r == sha256(body). + let expected: BytesN<32> = env.crypto().sha256(&body).into(); + let r: BytesN<32> = vaa + .slice(7..39) + .try_into() + .expect("signature 0 r-field must be 32 bytes"); + assert_eq!(r, expected, "invalid Guardian signature (body tampered)"); + + let emitter_chain = + (((body.get(8).unwrap() as u32) << 8) | (body.get(9).unwrap() as u32)) as u32; + let emitter_address: BytesN<32> = body + .slice(10..42) + .try_into() + .expect("emitter_address must be 32 bytes"); + let mut seq = [0u8; 8]; + let mut i = 0u32; + while i < 8 { + seq[i as usize] = body.get(42 + i).unwrap(); + i += 1; + } + let sequence = u64::from_be_bytes(seq); + let payload = body.slice(51..body.len()); + + VaaEnvelope { + emitter_chain, + emitter_address, + sequence, + payload, + } + } +} + // ─── Fixture ───────────────────────────────────────────────────────────────── struct Ctx { env: Env, admin: Address, - wormhole_core: Address, // placeholder; not called in mock + wormhole_core: Address, contract_id: Address, } @@ -40,8 +114,7 @@ fn setup() -> Ctx { env.mock_all_auths(); let admin = Address::generate(&env); - // The Wormhole Core address is stored but never called in this mock. - let wormhole_core = Address::generate(&env); + let wormhole_core = env.register_contract(None, MockWormholeCore); let contract_id = env.register_contract(None, ProofRegistry); let ctx = Ctx { @@ -50,14 +123,10 @@ fn setup() -> Ctx { wormhole_core, contract_id, }; - - ctx.client() - .initialize(&ctx.admin, &ctx.wormhole_core); - + ctx.client().initialize(&ctx.admin, &ctx.wormhole_core); ctx } -/// Build a deterministic `BytesN<32>` intent ID for use in tests. fn make_intent_id(env: &Env, seed: u8) -> BytesN<32> { let mut bytes = [0u8; 32]; bytes[0] = seed; @@ -65,61 +134,76 @@ fn make_intent_id(env: &Env, seed: u8) -> BytesN<32> { BytesN::from_array(env, &bytes) } -/// Build a minimal valid 102-byte VAA payload for `receive_message`. -/// -/// Layout: -/// [0..32] intent_id -/// [32..52] src_user (20 bytes, here all zeros except seed byte) -/// [52..54] src_chain_id (big-endian u16) -/// [54..86] src_token (32 bytes, all zeros) -/// [86..102] src_amount (big-endian i128) -fn make_payload(env: &Env, intent_id: &BytesN<32>, src_chain_id: u16, src_amount: i128) -> Bytes { - let mut payload = [0u8; 102]; - - // intent_id - let id_bytes = intent_id.to_array(); - payload[0..32].copy_from_slice(&id_bytes); - - // src_user: 20 zero bytes (placeholder) - // [32..52] already zeroed - - // src_chain_id (big-endian u16) - payload[52] = (src_chain_id >> 8) as u8; - payload[53] = (src_chain_id & 0xff) as u8; - - // src_token: 32 zero bytes (placeholder) - // [54..86] already zeroed - - // src_amount (big-endian i128) - let amount_bytes = src_amount.to_be_bytes(); - payload[86..102].copy_from_slice(&amount_bytes); - - Bytes::from_slice(env, &payload) +fn emitter_addr(env: &Env, tag: u8) -> BytesN<32> { + BytesN::from_array(env, &[tag; 32]) +} + +/// Build the fixed 102-byte Vortex deposit payload. +fn make_payload(intent_id: &BytesN<32>, src_chain_id: u16, src_amount: i128) -> [u8; 102] { + let mut p = [0u8; 102]; + p[0..32].copy_from_slice(&intent_id.to_array()); + // [32..52] src_user — left zeroed + p[52] = (src_chain_id >> 8) as u8; + p[53] = (src_chain_id & 0xff) as u8; + // [54..86] src_token — left zeroed + p[86..102].copy_from_slice(&src_amount.to_be_bytes()); + p +} + +/// Assemble a full VAA the [`MockWormholeCore`] will accept. +fn build_vaa( + env: &Env, + emitter_chain: u16, + emitter_address: &BytesN<32>, + sequence: u64, + payload: &[u8], +) -> Bytes { + let mut body = Bytes::new(env); + body.extend_from_array(&0u32.to_be_bytes()); // timestamp + body.extend_from_array(&0u32.to_be_bytes()); // nonce + body.extend_from_array(&emitter_chain.to_be_bytes()); + body.extend_from_array(&emitter_address.to_array()); + body.extend_from_array(&sequence.to_be_bytes()); + body.push_back(1u8); // consistency_level + body.append(&Bytes::from_slice(env, payload)); + + let hash: BytesN<32> = env.crypto().sha256(&body).into(); + + let mut vaa = Bytes::new(env); + vaa.push_back(1u8); // version + vaa.extend_from_array(&0u32.to_be_bytes()); // guardian_set_index + vaa.push_back(1u8); // signature_count + vaa.push_back(0u8); // sig 0: guardian_index + vaa.extend_from_array(&hash.to_array()); // sig 0: r == sha256(body) + vaa.extend_from_array(&[0u8; 32]); // sig 0: s + vaa.push_back(0u8); // sig 0: v + vaa.append(&body); + vaa } -// ─── Initialization ─────────────────────────────────────────────────────────── +// ─── Initialization ───────────────────────────────────────────────────────── #[test] fn initialize_succeeds_once() { let ctx = setup(); - // Double-initialize must fail. let res = ctx.client().try_initialize(&ctx.admin, &ctx.wormhole_core); assert_eq!(res, Err(Ok(Error::AlreadyInitialized.into()))); } -// ─── Authorized emitter management ─────────────────────────────────────────── - #[test] -fn set_and_get_authorized_emitter() { +fn initialize_records_wormhole_core() { let ctx = setup(); - let c = ctx.client(); + assert_eq!(ctx.client().get_wormhole_core(), Some(ctx.wormhole_core.clone())); +} - let emitter: BytesN<32> = BytesN::from_array(&ctx.env, &[0xde; 32]); - let chain_id: u32 = 2; // Wormhole Ethereum chain ID +// ─── Authorized emitter management ────────────────────────────────────────── - c.set_authorized_emitter(&chain_id, &emitter); - let stored = c.get_authorized_emitter(&chain_id); - assert_eq!(stored, Some(emitter)); +#[test] +fn set_and_get_authorized_emitter() { + let ctx = setup(); + let emitter = emitter_addr(&ctx.env, 0xde); + ctx.client().set_authorized_emitter(&2, &emitter); + assert_eq!(ctx.client().get_authorized_emitter(&2), Some(emitter)); } #[test] @@ -131,266 +215,272 @@ fn get_authorized_emitter_returns_none_if_unset() { #[test] fn remove_authorized_emitter_clears_entry() { let ctx = setup(); - let c = ctx.client(); - let emitter: BytesN<32> = BytesN::from_array(&ctx.env, &[0xab; 32]); - - c.set_authorized_emitter(&2, &emitter); - assert!(c.get_authorized_emitter(&2).is_some()); - - c.remove_authorized_emitter(&2); - assert_eq!(c.get_authorized_emitter(&2), None); + let emitter = emitter_addr(&ctx.env, 0xab); + ctx.client().set_authorized_emitter(&2, &emitter); + assert!(ctx.client().get_authorized_emitter(&2).is_some()); + ctx.client().remove_authorized_emitter(&2); + assert_eq!(ctx.client().get_authorized_emitter(&2), None); } -// ─── receive_message ───────────────────────────────────────────────────────── +// ─── receive_message: valid VAA ──────────────────────────────────────────── #[test] -fn receive_message_stores_proof() { +fn receive_message_stores_verified_proof() { let ctx = setup(); let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); + let intent_id = make_intent_id(&ctx.env, 1); - let payload = make_payload(&ctx.env, &intent_id, 2 /* Ethereum */, 1_000_000_000); + let payload = make_payload(&intent_id, 2, 1_000_000_000); + let vaa = build_vaa(&ctx.env, 2, &emitter, 4242, &payload); - c.receive_message(&payload); + c.receive_message(&vaa); assert!(c.has_proof(&intent_id)); - let record = c.get_proof(&intent_id).expect("proof should exist"); assert_eq!(record.intent_id, intent_id); assert_eq!(record.src_chain_id, 2); assert_eq!(record.src_amount, 1_000_000_000); + // vaa_sequence comes from the real VAA header, not a placeholder. + assert_eq!(record.vaa_sequence, 4242); } #[test] -fn receive_message_rejects_duplicate_intent_id() { +fn receive_message_decodes_large_src_amount() { let ctx = setup(); let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); - let intent_id = make_intent_id(&ctx.env, 2); - let payload = make_payload(&ctx.env, &intent_id, 2, 500_000_000); - - c.receive_message(&payload); + let intent_id = make_intent_id(&ctx.env, 3); + let eth_amount: i128 = 1_000_000_000_000_000_000; // 1 ETH in wei + let payload = make_payload(&intent_id, 2, eth_amount); + let vaa = build_vaa(&ctx.env, 2, &emitter, 1, &payload); - // Second call with the same intent_id must fail. - let res = c.try_receive_message(&payload); - assert_eq!(res, Err(Ok(Error::ProofAlreadyExists.into()))); + c.receive_message(&vaa); + assert_eq!(c.get_proof(&intent_id).unwrap().src_amount, eth_amount); } +// ─── receive_message: unauthorized emitter ───────────────────────────────── + #[test] -fn receive_message_rejects_wrong_payload_length() { +fn receive_message_rejects_unauthorized_emitter() { let ctx = setup(); + let c = ctx.client(); - // 50 bytes — too short. - let short = Bytes::from_slice(&ctx.env, &[0u8; 50]); - let res = ctx.client().try_receive_message(&short); - assert_eq!(res, Err(Ok(Error::InvalidPayload.into()))); + let authorized = emitter_addr(&ctx.env, 0xaa); + let attacker = emitter_addr(&ctx.env, 0xbb); + c.set_authorized_emitter(&2, &authorized); - // 200 bytes — too long. - let long = Bytes::from_slice(&ctx.env, &[0u8; 200]); - let res = ctx.client().try_receive_message(&long); - assert_eq!(res, Err(Ok(Error::InvalidPayload.into()))); + let intent_id = make_intent_id(&ctx.env, 4); + let payload = make_payload(&intent_id, 2, 500); + let vaa = build_vaa(&ctx.env, 2, &attacker, 1, &payload); + + let res = c.try_receive_message(&vaa); + assert_eq!(res, Err(Ok(Error::EmitterNotAuthorized.into()))); + assert!(!c.has_proof(&intent_id)); } #[test] -fn receive_message_decodes_src_amount_correctly() { +fn receive_message_rejects_when_no_emitter_configured_for_chain() { let ctx = setup(); let c = ctx.client(); - let intent_id = make_intent_id(&ctx.env, 3); - // Use a distinctive amount: 1 ETH in wei (18-decimal units). - let eth_amount: i128 = 1_000_000_000_000_000_000; - let payload = make_payload(&ctx.env, &intent_id, 2, eth_amount); + // chain 2 configured, VAA claims chain 30 — nothing authorized there. + c.set_authorized_emitter(&2, &emitter_addr(&ctx.env, 0xaa)); - c.receive_message(&payload); + let intent_id = make_intent_id(&ctx.env, 5); + let payload = make_payload(&intent_id, 30, 500); + let vaa = build_vaa(&ctx.env, 30, &emitter_addr(&ctx.env, 0xaa), 1, &payload); - let record = c.get_proof(&intent_id).unwrap(); - assert_eq!(record.src_amount, eth_amount); + let res = c.try_receive_message(&vaa); + assert_eq!(res, Err(Ok(Error::EmitterNotAuthorized.into()))); } -// ─── has_proof / get_proof ──────────────────────────────────────────────────── +// ─── receive_message: tampered payload ───────────────────────────────────── #[test] -fn has_proof_returns_false_for_unknown_intent() { +fn receive_message_rejects_tampered_payload() { let ctx = setup(); - let id = make_intent_id(&ctx.env, 99); - assert!(!ctx.client().has_proof(&id)); -} + let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); -#[test] -fn get_proof_returns_none_for_unknown_intent() { - let ctx = setup(); - let id = make_intent_id(&ctx.env, 99); - assert!(ctx.client().get_proof(&id).is_none()); + let intent_id = make_intent_id(&ctx.env, 6); + let payload = make_payload(&intent_id, 2, 1_000); + let vaa = build_vaa(&ctx.env, 2, &emitter, 1, &payload); + + // Flip one byte inside the signed body (the src_amount region of the + // payload) without re-signing. Core verification must trap. + let mut raw = [0u8; 512]; + let len = vaa.len(); + vaa.copy_into_slice(&mut raw[..len as usize]); + let tamper_idx = (len - 1) as usize; // last payload byte = low byte of src_amount + raw[tamper_idx] ^= 0xff; + let tampered = Bytes::from_slice(&ctx.env, &raw[..len as usize]); + + let res = c.try_receive_message(&tampered); + assert!(res.is_err(), "tampered VAA must be rejected at the Core boundary"); + assert!(!c.has_proof(&intent_id)); } -// ─── mock_set_proof (test back-door) ───────────────────────────────────────── +// ─── receive_message: replay protection ──────────────────────────────────── -/// `mock_set_proof` lets tests inject a fully controlled `ProofRecord` without -/// constructing a real VAA payload. #[test] -fn mock_set_proof_injects_controllable_record() { +fn receive_message_rejects_replayed_sequence() { let ctx = setup(); let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); - let intent_id = make_intent_id(&ctx.env, 10); + let intent_a = make_intent_id(&ctx.env, 7); + let payload_a = make_payload(&intent_a, 2, 111); + c.receive_message(&build_vaa(&ctx.env, 2, &emitter, 900, &payload_a)); - let record = ProofRecord { - intent_id: intent_id.clone(), - src_user: String::from_str(&ctx.env, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), - src_chain_id: 2, // Ethereum - src_token: String::from_str( - &ctx.env, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - ), - src_amount: 5_000 * 1_000_000, // 5 000 USDC (6 decimals) - vaa_sequence: 42, - received_at: ctx.env.ledger().timestamp(), - }; + // A different intent_id, but the same (emitter_chain, sequence) pair. + let intent_b = make_intent_id(&ctx.env, 8); + let payload_b = make_payload(&intent_b, 2, 222); + let res = c.try_receive_message(&build_vaa(&ctx.env, 2, &emitter, 900, &payload_b)); - c.mock_set_proof(&record); - - assert!(c.has_proof(&intent_id)); - let stored = c.get_proof(&intent_id).unwrap(); - assert_eq!(stored.src_amount, 5_000 * 1_000_000); - assert_eq!(stored.src_chain_id, 2); - assert_eq!(stored.vaa_sequence, 42); + assert_eq!(res, Err(Ok(Error::VaaAlreadyProcessed.into()))); + assert!(!c.has_proof(&intent_b)); } -/// Calling `mock_set_proof` twice for the same intent_id is rejected, matching -/// the replay protection in `receive_message`. #[test] -fn mock_set_proof_rejects_duplicate() { +fn receive_message_rejects_duplicate_intent_id() { let ctx = setup(); let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); - let intent_id = make_intent_id(&ctx.env, 11); + let intent_id = make_intent_id(&ctx.env, 9); + let payload = make_payload(&intent_id, 2, 333); + c.receive_message(&build_vaa(&ctx.env, 2, &emitter, 10, &payload)); - let record = ProofRecord { - intent_id: intent_id.clone(), - src_user: String::from_str(&ctx.env, "0xabc"), - src_chain_id: 30, // Base - src_token: String::from_str(&ctx.env, "0xdef"), - src_amount: 100, - vaa_sequence: 1, - received_at: 0, - }; + // Same intent_id, fresh sequence — caught by the per-intent_id guard. + let res = c.try_receive_message(&build_vaa(&ctx.env, 2, &emitter, 11, &payload)); + assert_eq!(res, Err(Ok(Error::ProofAlreadyExists.into()))); +} - c.mock_set_proof(&record.clone()); +// ─── receive_message: malformed / inconsistent ──────────────────────────── - let res = c.try_mock_set_proof(&record); - assert_eq!(res, Err(Ok(Error::ProofAlreadyExists.into()))); +#[test] +fn receive_message_rejects_chain_mismatch() { + let ctx = setup(); + let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); + + let intent_id = make_intent_id(&ctx.env, 10); + // Envelope says chain 2 (authorized) but the payload claims chain 30. + let payload = make_payload(&intent_id, 30, 1); + let vaa = build_vaa(&ctx.env, 2, &emitter, 1, &payload); + + let res = c.try_receive_message(&vaa); + assert_eq!(res, Err(Ok(Error::EmitterChainMismatch.into()))); } -/// `mock_remove_proof` clears a previously set proof, making `has_proof` -/// return false again. Useful for testing "proof not found" error paths. #[test] -fn mock_remove_proof_clears_stored_record() { +fn receive_message_rejects_wrong_payload_length() { let ctx = setup(); let c = ctx.client(); + let emitter = emitter_addr(&ctx.env, 0x11); + c.set_authorized_emitter(&2, &emitter); - let intent_id = make_intent_id(&ctx.env, 12); - let record = ProofRecord { - intent_id: intent_id.clone(), - src_user: String::from_str(&ctx.env, "0x1234"), - src_chain_id: 5, // Polygon - src_token: String::from_str(&ctx.env, "0x5678"), - src_amount: 999, - vaa_sequence: 7, - received_at: 0, - }; + // 80-byte payload instead of 102. + let vaa = build_vaa(&ctx.env, 2, &emitter, 1, &[0u8; 80]); + let res = c.try_receive_message(&vaa); + assert_eq!(res, Err(Ok(Error::InvalidPayload.into()))); +} - c.mock_set_proof(&record); - assert!(c.has_proof(&intent_id)); +#[test] +fn receive_message_traps_on_truncated_vaa() { + let ctx = setup(); + let short = Bytes::from_slice(&ctx.env, &[0u8; 4]); + let res = ctx.client().try_receive_message(&short); + assert!(res.is_err()); +} - c.mock_remove_proof(&intent_id); - assert!(!c.has_proof(&intent_id)); - assert!(c.get_proof(&intent_id).is_none()); +// ─── has_proof / get_proof ──────────────────────────────────────────────── + +#[test] +fn has_proof_returns_false_for_unknown_intent() { + let ctx = setup(); + assert!(!ctx.client().has_proof(&make_intent_id(&ctx.env, 99))); } -// ─── Scenario: proof-gated fill simulation ─────────────────────────────────── +#[test] +fn get_proof_returns_none_for_unknown_intent() { + let ctx = setup(); + assert!(ctx.client().get_proof(&make_intent_id(&ctx.env, 99)).is_none()); +} -/// Simulate the proof-gated fill flow at the oracle layer: -/// 1. A proof arrives via `mock_set_proof` (representing a Wormhole message). -/// 2. `intent_settlement` (simulated here as plain assertions) reads the proof -/// and validates the fields before allowing the fill. -/// -/// This test validates the oracle's query interface in isolation. The actual -/// cross-contract call from `intent_settlement` will be tested once -/// `fill_intent` gains the `require_proof` parameter. +// ─── mock_set_proof back-door (testutils only) ──────────────────────────── + +#[cfg(feature = "testutils")] #[test] -fn proof_gated_fill_simulation_positive_case() { +fn mock_set_proof_injects_controllable_record() { let ctx = setup(); let c = ctx.client(); - let intent_id = make_intent_id(&ctx.env, 20); - let expected_amount: i128 = 1_000_000_000_000_000_000; // 1 ETH in wei - let expected_chain: u32 = 2; // Wormhole Ethereum let record = ProofRecord { intent_id: intent_id.clone(), src_user: String::from_str(&ctx.env, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), - src_chain_id: expected_chain, - src_token: String::from_str( - &ctx.env, - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH - ), - src_amount: expected_amount, - vaa_sequence: 100, + src_chain_id: 2, + src_token: String::from_str(&ctx.env, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), + src_amount: 5_000 * 1_000_000, + vaa_sequence: 42, received_at: ctx.env.ledger().timestamp(), }; - c.mock_set_proof(&record); - // Simulate the checks intent_settlement.fill_intent would perform: - let proof = c.get_proof(&intent_id).expect("proof must exist for fill"); - assert_eq!(proof.src_chain_id, expected_chain, "chain ID must match intent"); - assert!( - proof.src_amount >= expected_amount, - "proof amount must meet or exceed intent src_amount" - ); + let stored = c.get_proof(&intent_id).unwrap(); + assert_eq!(stored.src_amount, 5_000 * 1_000_000); + assert_eq!(stored.vaa_sequence, 42); } -/// Negative case: if the proof's `src_amount` is less than the intent -/// required, the fill should be rejected. Simulated here by asserting the -/// check that `intent_settlement` will perform. +#[cfg(feature = "testutils")] #[test] -fn proof_gated_fill_simulation_amount_insufficient() { +fn mock_set_proof_rejects_duplicate() { let ctx = setup(); let c = ctx.client(); - let intent_id = make_intent_id(&ctx.env, 21); - let intent_src_amount: i128 = 2_000_000_000; // intent requires 2 000 USDC - let record = ProofRecord { intent_id: intent_id.clone(), src_user: String::from_str(&ctx.env, "0xabc"), - src_chain_id: 2, - src_token: String::from_str(&ctx.env, "0xusd"), - // Proof shows only 1 000 USDC deposited — less than the intent requires. - src_amount: 1_000_000_000, - vaa_sequence: 5, + src_chain_id: 30, + src_token: String::from_str(&ctx.env, "0xdef"), + src_amount: 100, + vaa_sequence: 1, received_at: 0, }; - - c.mock_set_proof(&record); - - let proof = c.get_proof(&intent_id).unwrap(); - // This is the check intent_settlement will perform; it must fail here. - assert!( - proof.src_amount < intent_src_amount, - "proof amount is insufficient — fill should be rejected" + c.mock_set_proof(&record.clone()); + assert_eq!( + c.try_mock_set_proof(&record), + Err(Ok(Error::ProofAlreadyExists.into())) ); } -/// Negative case: proof does not exist at all. -/// `intent_settlement` will call `get_proof` and receive `None`, then panic -/// with `ProofNotFound`. Verified here at the oracle layer. +#[cfg(feature = "testutils")] #[test] -fn proof_gated_fill_simulation_no_proof() { +fn mock_remove_proof_clears_stored_record() { let ctx = setup(); + let c = ctx.client(); let intent_id = make_intent_id(&ctx.env, 22); - - // No proof was set. - let proof = ctx.client().get_proof(&intent_id); - assert!(proof.is_none(), "no proof should exist for this intent"); + let record = ProofRecord { + intent_id: intent_id.clone(), + src_user: String::from_str(&ctx.env, "0x1234"), + src_chain_id: 5, + src_token: String::from_str(&ctx.env, "0x5678"), + src_amount: 999, + vaa_sequence: 7, + received_at: 0, + }; + c.mock_set_proof(&record); + assert!(c.has_proof(&intent_id)); + c.mock_remove_proof(&intent_id); + assert!(!c.has_proof(&intent_id)); } diff --git a/solver_registry/Cargo.lock b/solver_registry/Cargo.lock new file mode 100644 index 0000000..5544796 --- /dev/null +++ b/solver_registry/Cargo.lock @@ -0,0 +1,1634 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.2.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 = "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 = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.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 = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.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 = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.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 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.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 = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" +dependencies = [ + "backtrace", + "curve25519-dalek", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6edf92749fd8399b417192d301c11f710b9cdce15789a3d157785ea971576fa" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcdf04484af7cc731a7a48ad1d9f5f940370edeea84734434ceaf398a6b862e" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "derive_arbitrary", + "ed25519-dalek", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" +dependencies = [ + "crate-git-revision", + "darling 0.20.11", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn", +] + +[[package]] +name = "soroban-spec" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dafbde981b141b191c6c036abc86097070ddd6eaaa33b273701449501e43d3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.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 = "vortex-solver-registry" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[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" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/solver_registry/Cargo.toml b/solver_registry/Cargo.toml new file mode 100644 index 0000000..2f72f46 --- /dev/null +++ b/solver_registry/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "vortex-solver-registry" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 + +[dependencies] +soroban-sdk = { version = "21.0.0" } + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] diff --git a/solver_registry/src/lib.rs b/solver_registry/src/lib.rs new file mode 100644 index 0000000..213058b --- /dev/null +++ b/solver_registry/src/lib.rs @@ -0,0 +1,732 @@ +#![no_std] + +//! Vortex Protocol — Solver Registry (`solver_registry`) +//! +//! Standalone Soroban contract implementing the tiered-staking model from +//! [`docs/solver-registry-design.md`] and the public ABI in +//! [`docs/solver-registry-interface.md`]. +//! +//! It follows **Option A** of the design doc (§4): `solver_registry` is the +//! canonical store for a solver's bond and fill history, and derives a +//! reputation score and a bond/score-gated **tier** (0 = Unranked … 4 = +//! Platinum) from that state. `intent_settlement` is expected to *read* the +//! tier and perk views from here (and, once wired up, to feed fill outcomes in +//! via [`SolverRegistry::record_fill`] / [`record_failure`] / [`slash`]). +//! Actually consuming the tier perks inside `intent_settlement` +//! (`accept_intent` / `slash_solver`) is a deliberately separate follow-up — +//! only the read interface is defined here, and it is meant to stay stable. +//! +//! The reputation formula is ported **byte-for-byte** from +//! `intent_settlement::compute_reputation_score`; `score_test_vector` in the +//! test module is the shared cross-check. + +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, panic_with_error, token, Address, Env, + Symbol, Vec, +}; + +#[cfg(test)] +mod test; + +// ─── Units & tier defaults ─────────────────────────────────────────────────── + +/// Smallest unit of the 7-decimal USDC bond token (1 USDC = 10_000_000). +pub const USDC: i128 = 10_000_000; + +/// Number of tiers (0..=4). +pub const TIER_COUNT: u32 = 5; + +/// Human-readable tier names, indexed by tier level. +pub const TIER_NAMES: [&str; 5] = ["Unranked", "Bronze", "Silver", "Gold", "Platinum"]; + +/// Default `(min_bond_usdc, min_score_bps)` per tier — the 5-row table from +/// design doc §3. Stored in instance storage at `initialize` and tunable +/// (within bounds) via [`SolverRegistry::set_tier_threshold`]. +const DEFAULT_THRESHOLDS: [(i128, u32); 5] = [ + (50, 0), // 0 Unranked + (500, 1_000), // 1 Bronze + (2_000, 3_500), // 2 Silver + (10_000, 7_000),// 3 Gold + (50_000, 9_000),// 4 Platinum +]; + +/// Fixed (non-tunable) perk: fill-window extension in percent, per tier +/// (design §3 / §6). +const FILL_WINDOW_BONUS_PCT: [u32; 5] = [0, 10, 20, 30, 50]; + +/// Fixed (non-tunable) perk: slash size in basis points of bond, per tier +/// (design §3 / §7). Tier 0 and 1 pay the full 10%. +const SLASH_BPS: [u32; 5] = [1_000, 1_000, 800, 600, 500]; + +/// Fixed perk slot: fee rebate in basis points, per tier (design §3 / §8). +/// Left at 0 pending the tokenomics review — the slot is reserved so the ABI +/// does not change when the numbers land. +const FEE_REBATE_BPS: [u32; 5] = [0, 0, 0, 0, 0]; + +// ─── Tuning bounds for set_tier_threshold ──────────────────────────────────── + +/// A tunable `min_bond` may not exceed 1,000,000 USDC. +const MAX_TUNABLE_MIN_BOND: i128 = 1_000_000 * USDC; +/// The maximum achievable score is 9,999 bps by construction, so a threshold +/// above that would make a tier unreachable. +const MAX_TUNABLE_MIN_SCORE_BPS: u32 = 9_999; + +// ─── TTL bumping (see docs/ttl-constants-rationale.md) ─────────────────────── + +const DAY_IN_LEDGERS: u32 = 17_280; // ~5s per ledger +const PERSISTENT_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 14; +const PERSISTENT_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; +const INSTANCE_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; +const INSTANCE_TTL_EXTEND_TO: u32 = DAY_IN_LEDGERS * 60; + +// ─── Storage ───────────────────────────────────────────────────────────────── + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + /// Instance: admin `Address` (set in `initialize`). + Admin, + /// Instance: bond token (`Address`) solvers stake. + BondToken, + /// Instance: `Address` that receives slashed bond. + FeeRecipient, + /// Instance: the settlement contract authorized to call `record_fill` / + /// `record_failure` / `slash`. Absent until `set_writer`. + Writer, + /// Instance: `Vec` of length 5 — the effective tier table. + Thresholds, + /// Instance: registered-solver count (`u32`). + TotalSolvers, + /// Persistent: per-solver record. + Solver(Address), +} + +/// One row of the tunable part of the tier table. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct TierThreshold { + /// Minimum bond, in the bond token's smallest unit. + pub min_bond: i128, + /// Minimum reputation score, in basis points (0..=10_000). + pub min_score_bps: u32, +} + +/// Full tier-table row returned by [`SolverRegistry::get_tier_table`]. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct TierInfo { + pub tier: u32, + pub name: Symbol, + pub min_bond: i128, + pub min_score_bps: u32, + pub fill_window_bonus_pct: u32, + pub slash_bps: u32, + pub fee_rebate_bps: u32, +} + +/// Canonical per-solver state (design §4, Option A). +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct SolverRecord { + pub address: Address, + /// Bond currently staked, in the bond token's smallest unit. + pub bond_amount: i128, + pub fills_completed: u32, + pub fills_failed: u32, + pub total_volume: i128, + pub registered_at: u64, + /// Ledger timestamp of the most recent slash (0 = never). + pub last_slash_time: u64, + /// Cumulative bond taken by slashing over this solver's lifetime. + pub slashed_total: i128, +} + +// ─── Errors ────────────────────────────────────────────────────────────────── + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + SolverNotRegistered = 4, + SolverAlreadyRegistered = 5, + /// Bond would fall below the tier-0 (`Unranked`) floor. + BondBelowFloor = 6, + /// A positive amount was required but zero/negative was supplied. + ZeroAmount = 7, + /// Unstake amount exceeds the staked bond. + InsufficientBond = 8, + /// Tier index outside 0..=4, or tier 0 passed to `set_tier_threshold`. + InvalidTier = 9, + /// Threshold value outside its documented bound. + ThresholdOutOfBounds = 10, + /// Thresholds must strictly increase from one tier to the next. + ThresholdsNotMonotonic = 11, + /// `record_fill` / `record_failure` / `slash` called before `set_writer` + /// with a caller that is not the admin. + WriterNotSet = 12, +} + +// ─── Reputation formula ────────────────────────────────────────────────────── + +/// Reputation score in basis points (0..=10_000). +/// +/// **Ported byte-for-byte from `intent_settlement::compute_reputation_score`** +/// — the two implementations must not drift. `score_test_vector` in the test +/// module pins the shared input→output vector referenced by +/// `docs/solver-registry-design.md` §2. +/// +/// ```text +/// total = fills_completed + fills_failed (0 → score 0) +/// base = fills_completed * 10_000 / total success rate, 0..10_000 bps +/// decay = VOLUME_SCALE * 10_000 / (VOLUME_SCALE + vol + 1) +/// mult = 10_000 - decay / 10 9_000..10_000 bps +/// score = base * mult / 10_000 +/// ``` +fn score_of(record: &SolverRecord) -> u32 { + let total_fills = record.fills_completed as u64 + record.fills_failed as u64; + if total_fills == 0 { + return 0; + } + + let base_bps = (record.fills_completed as u64 * 10_000) / total_fills; + + const VOLUME_SCALE: i128 = 1_000 * 100 * 10_000_000; + + let vol = record.total_volume.max(0); + let decay_bps = ((VOLUME_SCALE as u64) * 10_000) / ((VOLUME_SCALE + vol + 1) as u64); + + let multiplier_bps = 10_000u64 - decay_bps / 10; + + let score = base_bps * multiplier_bps / 10_000; + score as u32 +} + +// ─── Contract ──────────────────────────────────────────────────────────────── + +#[contract] +pub struct SolverRegistry; + +#[contractimpl] +impl SolverRegistry { + // ── Initialization ─────────────────────────────────────────────────────── + + /// One-time setup. Seeds the tier table from [`DEFAULT_THRESHOLDS`] + /// (converting the USDC figures to the token's smallest unit). + pub fn initialize(env: Env, admin: Address, bond_token: Address, fee_recipient: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::BondToken, &bond_token); + env.storage() + .instance() + .set(&DataKey::FeeRecipient, &fee_recipient); + env.storage().instance().set(&DataKey::TotalSolvers, &0u32); + + let mut rows: Vec = Vec::new(&env); + for (min_bond_usdc, min_score_bps) in DEFAULT_THRESHOLDS.iter() { + rows.push_back(TierThreshold { + min_bond: *min_bond_usdc * USDC, + min_score_bps: *min_score_bps, + }); + } + env.storage().instance().set(&DataKey::Thresholds, &rows); + Self::bump_instance_ttl(&env); + } + + // ── Admin ──────────────────────────────────────────────────────────────── + + /// Admin-only: set (or rotate) the settlement contract permitted to call + /// `record_fill` / `record_failure` / `slash`. + pub fn set_writer(env: Env, writer: Address) { + Self::require_admin(&env); + env.storage().instance().set(&DataKey::Writer, &writer); + Self::bump_instance_ttl(&env); + env.events() + .publish((Symbol::new(&env, "writer_set"),), writer); + } + + /// The configured settlement writer, if any. + pub fn get_writer(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::Writer) + } + + /// Admin-only: tune one tier's `min_bond` / `min_score_bps`. + /// + /// Bounds (any violation → `ThresholdOutOfBounds` / `ThresholdsNotMonotonic` + /// / `InvalidTier`): + /// * `tier` ∈ 1..=4 (tier 0 is the fixed entry floor). + /// * `min_bond` ∈ [1, [`MAX_TUNABLE_MIN_BOND`]]. + /// * `min_score_bps` ≤ [`MAX_TUNABLE_MIN_SCORE_BPS`]. + /// * Row must stay strictly greater than tier `tier-1` and strictly less + /// than tier `tier+1` on **both** axes. + pub fn set_tier_threshold(env: Env, tier: u32, min_bond: i128, min_score_bps: u32) { + Self::require_admin(&env); + + if tier == 0 || tier >= TIER_COUNT { + panic_with_error!(&env, Error::InvalidTier); + } + if min_bond < 1 || min_bond > MAX_TUNABLE_MIN_BOND { + panic_with_error!(&env, Error::ThresholdOutOfBounds); + } + if min_score_bps > MAX_TUNABLE_MIN_SCORE_BPS { + panic_with_error!(&env, Error::ThresholdOutOfBounds); + } + + let mut rows: Vec = env + .storage() + .instance() + .get(&DataKey::Thresholds) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + + let below = rows.get(tier - 1).unwrap(); + if min_bond <= below.min_bond || min_score_bps <= below.min_score_bps { + panic_with_error!(&env, Error::ThresholdsNotMonotonic); + } + if tier + 1 < TIER_COUNT { + let above = rows.get(tier + 1).unwrap(); + if min_bond >= above.min_bond || min_score_bps >= above.min_score_bps { + panic_with_error!(&env, Error::ThresholdsNotMonotonic); + } + } + + rows.set( + tier, + TierThreshold { + min_bond, + min_score_bps, + }, + ); + env.storage().instance().set(&DataKey::Thresholds, &rows); + Self::bump_instance_ttl(&env); + + env.events().publish( + (Symbol::new(&env, "tier_threshold_set"),), + (tier, min_bond, min_score_bps), + ); + } + + // ── Solver self-service ────────────────────────────────────────────────── + + /// Register as a solver by staking `bond_amount` (must clear the tier-0 + /// floor). The bond token is pulled from `solver`. + pub fn register_solver(env: Env, solver: Address, bond_amount: i128) { + solver.require_auth(); + + if bond_amount <= 0 { + panic_with_error!(&env, Error::ZeroAmount); + } + if env + .storage() + .persistent() + .has(&DataKey::Solver(solver.clone())) + { + panic_with_error!(&env, Error::SolverAlreadyRegistered); + } + if bond_amount < Self::tier0_floor(&env) { + panic_with_error!(&env, Error::BondBelowFloor); + } + + let record = SolverRecord { + address: solver.clone(), + bond_amount, + fills_completed: 0, + fills_failed: 0, + total_volume: 0, + registered_at: env.ledger().timestamp(), + last_slash_time: 0, + slashed_total: 0, + }; + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + + let total: u32 = env + .storage() + .instance() + .get(&DataKey::TotalSolvers) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSolvers, &(total + 1)); + + Self::bond_client(&env).transfer( + &solver, + &env.current_contract_address(), + &bond_amount, + ); + + env.events() + .publish((Symbol::new(&env, "solver_registered"), solver), bond_amount); + } + + /// Add `amount` to an existing solver's bond. + pub fn stake(env: Env, solver: Address, amount: i128) { + solver.require_auth(); + if amount <= 0 { + panic_with_error!(&env, Error::ZeroAmount); + } + let mut record = Self::load_solver(&env, &solver); + record.bond_amount += amount; + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + + Self::bond_client(&env).transfer( + &solver, + &env.current_contract_address(), + &amount, + ); + env.events().publish( + (Symbol::new(&env, "staked"), solver), + (amount, record.bond_amount), + ); + } + + /// Withdraw `amount` of bond. The remaining balance must still clear the + /// tier-0 floor — use [`SolverRegistry::deregister_solver`] to exit fully. + pub fn unstake(env: Env, solver: Address, amount: i128) { + solver.require_auth(); + if amount <= 0 { + panic_with_error!(&env, Error::ZeroAmount); + } + let mut record = Self::load_solver(&env, &solver); + if amount > record.bond_amount { + panic_with_error!(&env, Error::InsufficientBond); + } + let remaining = record.bond_amount - amount; + if remaining < Self::tier0_floor(&env) { + panic_with_error!(&env, Error::BondBelowFloor); + } + record.bond_amount = remaining; + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + + Self::bond_client(&env).transfer( + &env.current_contract_address(), + &solver, + &amount, + ); + env.events() + .publish((Symbol::new(&env, "unstaked"), solver), (amount, remaining)); + } + + /// Exit the registry: returns the full remaining bond and deletes the + /// record. + pub fn deregister_solver(env: Env, solver: Address) { + solver.require_auth(); + let record = Self::load_solver(&env, &solver); + + env.storage() + .persistent() + .remove(&DataKey::Solver(solver.clone())); + let total: u32 = env + .storage() + .instance() + .get(&DataKey::TotalSolvers) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSolvers, &total.saturating_sub(1)); + + if record.bond_amount > 0 { + Self::bond_client(&env).transfer( + &env.current_contract_address(), + &solver, + &record.bond_amount, + ); + } + env.events().publish( + (Symbol::new(&env, "solver_deregistered"), solver), + record.bond_amount, + ); + } + + // ── Settlement write path (writer or admin) ───────────────────────────── + + /// Record a successful fill of `amount` (dst-token units) by `solver`. + /// `caller` must be the configured writer or the admin. + pub fn record_fill(env: Env, caller: Address, solver: Address, amount: i128) { + Self::require_writer_or_admin(&env, &caller); + if amount < 0 { + panic_with_error!(&env, Error::ZeroAmount); + } + let mut record = Self::load_solver(&env, &solver); + record.fills_completed += 1; + record.total_volume += amount; + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + env.events().publish( + (Symbol::new(&env, "fill_recorded"), solver), + (amount, score_of(&record)), + ); + } + + /// Record a failed fill by `solver` (no slash — that is `slash`). + /// `caller` must be the configured writer or the admin. + pub fn record_failure(env: Env, caller: Address, solver: Address) { + Self::require_writer_or_admin(&env, &caller); + let mut record = Self::load_solver(&env, &solver); + record.fills_failed += 1; + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + env.events().publish( + (Symbol::new(&env, "failure_recorded"), solver), + score_of(&record), + ); + } + + /// Slash `solver`'s bond by the tier-dependent [`SLASH_BPS`] (minimum 1 + /// unit), transfer it to the fee recipient, and record a failed fill. + /// + /// Returns `(slash_amount, new_tier)`. `caller` must be the configured + /// writer or the admin. + pub fn slash(env: Env, caller: Address, solver: Address) -> (i128, u32) { + Self::require_writer_or_admin(&env, &caller); + let mut record = Self::load_solver(&env, &solver); + + let tier_before = Self::tier_of(&env, &record); + let bps = SLASH_BPS[tier_before as usize] as i128; + let slash_amount = if record.bond_amount > 0 { + (record.bond_amount * bps / 10_000).max(1) + } else { + 0 + }; + + record.bond_amount -= slash_amount; + record.fills_failed += 1; + record.slashed_total += slash_amount; + record.last_slash_time = env.ledger().timestamp(); + env.storage() + .persistent() + .set(&DataKey::Solver(solver.clone()), &record); + Self::bump_solver_ttl(&env, &solver); + + if slash_amount > 0 { + let fee_recipient: Address = env + .storage() + .instance() + .get(&DataKey::FeeRecipient) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + Self::bond_client(&env).transfer( + &env.current_contract_address(), + &fee_recipient, + &slash_amount, + ); + } + + let new_tier = Self::tier_of(&env, &record); + env.events().publish( + (Symbol::new(&env, "solver_slashed"), solver), + (slash_amount, new_tier), + ); + (slash_amount, new_tier) + } + + // ── Views ─────────────────────────────────────────────────────────────── + + /// Current tier (0..=4) for `solver`. Unknown solver → 0. + pub fn get_tier(env: Env, solver: Address) -> u32 { + match env + .storage() + .persistent() + .get::<_, SolverRecord>(&DataKey::Solver(solver)) + { + Some(record) => Self::tier_of(&env, &record), + None => 0, + } + } + + /// Pure tier lookup for an arbitrary `(score_bps, bond_amount)` pair — lets + /// off-chain solver bots price perks without a stored record. + pub fn tier_for(env: Env, score_bps: u32, bond_amount: i128) -> u32 { + let rows = Self::thresholds(&env); + let mut tier = 0u32; + let mut i = 0u32; + while i < TIER_COUNT { + let row = rows.get(i).unwrap(); + if bond_amount >= row.min_bond && score_bps >= row.min_score_bps { + tier = i; + } + i += 1; + } + tier + } + + /// Reputation score (0..=10_000 bps) for `solver`, or `None` if unknown. + pub fn get_reputation_score(env: Env, solver: Address) -> Option { + let record: SolverRecord = env + .storage() + .persistent() + .get(&DataKey::Solver(solver))?; + Some(score_of(&record)) + } + + /// Full per-solver record, or `None`. + pub fn get_solver(env: Env, solver: Address) -> Option { + env.storage().persistent().get(&DataKey::Solver(solver)) + } + + /// Count of currently registered solvers. + pub fn get_solver_count(env: Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::TotalSolvers) + .unwrap_or(0) + } + + /// The effective 5-row tier table (tunable thresholds + fixed perks). + pub fn get_tier_table(env: Env) -> Vec { + let rows = Self::thresholds(&env); + let mut out: Vec = Vec::new(&env); + let mut i = 0u32; + while i < TIER_COUNT { + let row = rows.get(i).unwrap(); + out.push_back(TierInfo { + tier: i, + name: Symbol::new(&env, TIER_NAMES[i as usize]), + min_bond: row.min_bond, + min_score_bps: row.min_score_bps, + fill_window_bonus_pct: FILL_WINDOW_BONUS_PCT[i as usize], + slash_bps: SLASH_BPS[i as usize], + fee_rebate_bps: FEE_REBATE_BPS[i as usize], + }); + i += 1; + } + out + } + + /// Fixed perk: fill-window extension percent for `tier`. + pub fn get_fill_window_bonus_pct(env: Env, tier: u32) -> u32 { + Self::perk(&env, tier, &FILL_WINDOW_BONUS_PCT) + } + + /// Fixed perk: slash size in bps of bond for `tier`. + pub fn get_slash_bps(env: Env, tier: u32) -> u32 { + Self::perk(&env, tier, &SLASH_BPS) + } + + /// Reserved perk slot: fee rebate in bps for `tier` (currently 0). + pub fn get_fee_rebate_bps(env: Env, tier: u32) -> u32 { + Self::perk(&env, tier, &FEE_REBATE_BPS) + } + + /// Admin address, or `None` before initialization. + pub fn get_admin(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::Admin) + } + + /// Bond token address, or `None` before initialization. + pub fn get_bond_token(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::BondToken) + } + + // ── Reputation formula ───────────────────────────────────────────────── + + /// Reputation score (0..=10_000 bps) for a solver record. Callable view; + /// see [`score_of`] for the implementation the rest of the contract uses. + pub fn compute_reputation_score(record: SolverRecord) -> u32 { + score_of(&record) + } + + // ── Internal ────────────────────────────────────────────────────────── + + fn tier_of(env: &Env, record: &SolverRecord) -> u32 { + let score = score_of(record); + Self::tier_for(env.clone(), score, record.bond_amount) + } + + fn thresholds(env: &Env) -> Vec { + env.storage() + .instance() + .get(&DataKey::Thresholds) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)) + } + + fn tier0_floor(env: &Env) -> i128 { + Self::thresholds(env).get(0).unwrap().min_bond + } + + fn perk(env: &Env, tier: u32, table: &[u32; 5]) -> u32 { + if tier >= TIER_COUNT { + panic_with_error!(env, Error::InvalidTier); + } + table[tier as usize] + } + + fn load_solver(env: &Env, solver: &Address) -> SolverRecord { + env.storage() + .persistent() + .get(&DataKey::Solver(solver.clone())) + .unwrap_or_else(|| panic_with_error!(env, Error::SolverNotRegistered)) + } + + fn bond_client<'a>(env: &Env) -> token::Client<'a> { + let bond_token: Address = env + .storage() + .instance() + .get(&DataKey::BondToken) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + token::Client::new(env, &bond_token) + } + + fn require_admin(env: &Env) { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + admin.require_auth(); + } + + fn require_writer_or_admin(env: &Env, caller: &Address) { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + let writer: Option
= env.storage().instance().get(&DataKey::Writer); + match writer { + Some(w) => { + if *caller != w && *caller != admin { + panic_with_error!(env, Error::Unauthorized); + } + } + None => { + if *caller != admin { + // No writer configured yet: only the admin may drive the + // write path. + panic_with_error!(env, Error::WriterNotSet); + } + } + } + caller.require_auth(); + } + + fn bump_instance_ttl(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + + fn bump_solver_ttl(env: &Env, solver: &Address) { + env.storage().persistent().extend_ttl( + &DataKey::Solver(solver.clone()), + PERSISTENT_TTL_THRESHOLD, + PERSISTENT_TTL_EXTEND_TO, + ); + } +} diff --git a/solver_registry/src/test.rs b/solver_registry/src/test.rs new file mode 100644 index 0000000..3ef8cbb --- /dev/null +++ b/solver_registry/src/test.rs @@ -0,0 +1,423 @@ +#![cfg(test)] + +//! Test suite for `solver_registry`. +//! +//! Covers: tier-table seeding, registration/stake/unstake/deregister, the +//! settlement write path (`record_fill` / `record_failure` / `slash`), tier +//! boundary transitions (score exactly on a threshold), tier demotion on +//! slash, the zero-fills edge case, and threshold tuning bounds. + +use crate::{Error, SolverRecord, SolverRegistry, SolverRegistryClient, USDC}; +use soroban_sdk::{ + testutils::Address as _, token, Address, Env, +}; + +const FLOOR: i128 = 50 * USDC; // tier-0 (Unranked) bond floor + +struct Ctx { + env: Env, + admin: Address, + fee_recipient: Address, + solver: Address, + bond_token: Address, + contract_id: Address, +} + +impl Ctx { + fn client(&self) -> SolverRegistryClient<'_> { + SolverRegistryClient::new(&self.env, &self.contract_id) + } + fn bond(&self) -> token::Client<'_> { + token::Client::new(&self.env, &self.bond_token) + } + fn mint(&self, to: &Address, amount: i128) { + token::StellarAssetClient::new(&self.env, &self.bond_token).mint(to, &amount); + } + /// Mint `bond` to the default solver and register them. + fn register(&self, bond: i128) { + self.mint(&self.solver, bond); + self.client().register_solver(&self.solver, &bond); + } +} + +fn setup() -> Ctx { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let fee_recipient = Address::generate(&env); + let solver = Address::generate(&env); + let bond_token = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + let contract_id = env.register_contract(None, SolverRegistry); + + let ctx = Ctx { + env, + admin, + fee_recipient, + solver, + bond_token, + contract_id, + }; + ctx.client() + .initialize(&ctx.admin, &ctx.bond_token, &ctx.fee_recipient); + ctx +} + +// ─── Initialization ───────────────────────────────────────────────────────── + +#[test] +fn initialize_seeds_the_design_doc_tier_table() { + let ctx = setup(); + let table = ctx.client().get_tier_table(); + assert_eq!(table.len(), 5); + + let r0 = table.get(0).unwrap(); + assert_eq!(r0.min_bond, 50 * USDC); + assert_eq!(r0.min_score_bps, 0); + assert_eq!(r0.fill_window_bonus_pct, 0); + assert_eq!(r0.slash_bps, 1_000); + + let r4 = table.get(4).unwrap(); + assert_eq!(r4.min_bond, 50_000 * USDC); + assert_eq!(r4.min_score_bps, 9_000); + assert_eq!(r4.fill_window_bonus_pct, 50); + assert_eq!(r4.slash_bps, 500); + assert_eq!(r4.fee_rebate_bps, 0); // reserved slot + + assert_eq!(ctx.client().get_admin(), Some(ctx.admin.clone())); + assert_eq!(ctx.client().get_bond_token(), Some(ctx.bond_token.clone())); +} + +#[test] +fn cannot_initialize_twice() { + let ctx = setup(); + let res = ctx + .client() + .try_initialize(&ctx.admin, &ctx.bond_token, &ctx.fee_recipient); + assert_eq!(res, Err(Ok(Error::AlreadyInitialized.into()))); +} + +// ─── Registration / staking ──────────────────────────────────────────────── + +#[test] +fn register_rejects_bond_below_floor() { + let ctx = setup(); + ctx.mint(&ctx.solver, FLOOR); + let res = ctx.client().try_register_solver(&ctx.solver, &(FLOOR - 1)); + assert_eq!(res, Err(Ok(Error::BondBelowFloor.into()))); +} + +#[test] +fn register_rejects_zero() { + let ctx = setup(); + let res = ctx.client().try_register_solver(&ctx.solver, &0); + assert_eq!(res, Err(Ok(Error::ZeroAmount.into()))); +} + +#[test] +fn register_locks_bond_and_counts_solver() { + let ctx = setup(); + ctx.register(FLOOR); + assert_eq!(ctx.client().get_solver_count(), 1); + assert_eq!(ctx.bond().balance(&ctx.contract_id), FLOOR); + assert_eq!(ctx.bond().balance(&ctx.solver), 0); + // No fills yet → score 0 → Unranked. + assert_eq!(ctx.client().get_tier(&ctx.solver), 0); +} + +#[test] +fn register_twice_rejected() { + let ctx = setup(); + ctx.register(FLOOR); + ctx.mint(&ctx.solver, FLOOR); + let res = ctx.client().try_register_solver(&ctx.solver, &FLOOR); + assert_eq!(res, Err(Ok(Error::SolverAlreadyRegistered.into()))); +} + +#[test] +fn stake_then_unstake_back_to_floor() { + let ctx = setup(); + ctx.register(FLOOR); + ctx.mint(&ctx.solver, 1_000 * USDC); + ctx.client().stake(&ctx.solver, &(1_000 * USDC)); + assert_eq!(ctx.client().get_solver(&ctx.solver).unwrap().bond_amount, FLOOR + 1_000 * USDC); + + ctx.client().unstake(&ctx.solver, &(1_000 * USDC)); + assert_eq!(ctx.client().get_solver(&ctx.solver).unwrap().bond_amount, FLOOR); + + // One more unit would drop below the floor. + let res = ctx.client().try_unstake(&ctx.solver, &1); + assert_eq!(res, Err(Ok(Error::BondBelowFloor.into()))); +} + +#[test] +fn unstake_more_than_bond_rejected() { + let ctx = setup(); + ctx.register(FLOOR); + let res = ctx.client().try_unstake(&ctx.solver, &(FLOOR + 1)); + assert_eq!(res, Err(Ok(Error::InsufficientBond.into()))); +} + +#[test] +fn deregister_returns_full_bond() { + let ctx = setup(); + ctx.register(100 * USDC); + ctx.client().deregister_solver(&ctx.solver); + assert_eq!(ctx.bond().balance(&ctx.solver), 100 * USDC); + assert!(ctx.client().get_solver(&ctx.solver).is_none()); + assert_eq!(ctx.client().get_solver_count(), 0); +} + +// ─── Reputation formula ──────────────────────────────────────────────────── + +/// Shared input→output vector. `intent_settlement::compute_reputation_score` +/// MUST produce the identical values for the same inputs — this is the +/// cross-check referenced by the design doc. +#[test] +fn score_test_vector() { + let env = Env::default(); + let who = Address::generate(&env); + let rec = |completed: u32, failed: u32, vol: i128| SolverRecord { + address: who.clone(), + bond_amount: 0, + fills_completed: completed, + fills_failed: failed, + total_volume: vol, + registered_at: 0, + last_slash_time: 0, + slashed_total: 0, + }; + + // no activity → 0 + assert_eq!(SolverRegistry::compute_reputation_score(rec(0, 0, 0)), 0); + // all failures → 0 + assert_eq!(SolverRegistry::compute_reputation_score(rec(0, 5, 0)), 0); + // perfect record, no volume → 9_001 (90% floor + integer rounding) + assert_eq!(SolverRegistry::compute_reputation_score(rec(1, 0, 0)), 9_001); + // 8/10 success, no volume → 7_200 (0.8 * 9_001, truncated) + assert_eq!(SolverRegistry::compute_reputation_score(rec(8, 2, 0)), 7_200); + // 8/10 success, very high volume → decay ≈ 0, multiplier ≈ 10_000 → 8_000 + assert_eq!( + SolverRegistry::compute_reputation_score(rec(8, 2, 100_000_000 * USDC)), + 8_000 + ); +} + +#[test] +fn get_reputation_score_none_for_unknown() { + let ctx = setup(); + assert_eq!(ctx.client().get_reputation_score(&ctx.solver), None); +} + +// ─── Tier boundaries (pure lookup) ───────────────────────────────────────── + +#[test] +fn tier_for_hits_each_threshold_exactly() { + let ctx = setup(); + let c = ctx.client(); + + // Unranked + assert_eq!(c.tier_for(&0, &FLOOR), 0); + assert_eq!(c.tier_for(&0, &(FLOOR - 1)), 0); + + // Bronze: score 1_000 bps AND bond 500 USDC + assert_eq!(c.tier_for(&1_000, &(500 * USDC)), 1); + assert_eq!(c.tier_for(&999, &(500 * USDC)), 0); // score one bp short + assert_eq!(c.tier_for(&1_000, &(500 * USDC - 1)), 0); // bond one unit short + + // Silver / Gold + assert_eq!(c.tier_for(&3_500, &(2_000 * USDC)), 2); + assert_eq!(c.tier_for(&3_499, &(2_000 * USDC)), 1); + assert_eq!(c.tier_for(&7_000, &(10_000 * USDC)), 3); + + // Platinum + assert_eq!(c.tier_for(&9_000, &(50_000 * USDC)), 4); + assert_eq!(c.tier_for(&8_999, &(50_000 * USDC)), 3); + // Score above the (unreachable) 10_000 ceiling still resolves fine. + assert_eq!(c.tier_for(&10_000, &(60_000 * USDC)), 4); +} + +// ─── Zero-fills edge case ────────────────────────────────────────────────── + +#[test] +fn zero_fills_pins_tier_to_unranked_regardless_of_bond() { + let ctx = setup(); + // Bond large enough for Platinum, but no fills → score 0 → tier 0. + ctx.register(60_000 * USDC); + assert_eq!(ctx.client().get_reputation_score(&ctx.solver), Some(0)); + assert_eq!(ctx.client().get_tier(&ctx.solver), 0); +} + +// ─── Settlement write path ───────────────────────────────────────────────── + +#[test] +fn record_fill_updates_volume_and_score() { + let ctx = setup(); + ctx.register(FLOOR); + // No writer configured yet → admin drives the write path. + ctx.client() + .record_fill(&ctx.admin, &ctx.solver, &(100 * USDC)); + + let rec = ctx.client().get_solver(&ctx.solver).unwrap(); + assert_eq!(rec.fills_completed, 1); + assert_eq!(rec.total_volume, 100 * USDC); + assert!(ctx.client().get_reputation_score(&ctx.solver).unwrap() > 0); +} + +#[test] +fn record_failure_lowers_score() { + let ctx = setup(); + ctx.register(FLOOR); + ctx.client().record_fill(&ctx.admin, &ctx.solver, &0); + let before = ctx.client().get_reputation_score(&ctx.solver).unwrap(); + ctx.client().record_failure(&ctx.admin, &ctx.solver); + let after = ctx.client().get_reputation_score(&ctx.solver).unwrap(); + assert!(after < before, "{after} !< {before}"); +} + +#[test] +fn writer_can_drive_write_path_and_strangers_cannot() { + let ctx = setup(); + ctx.register(FLOOR); + let writer = Address::generate(&ctx.env); + let stranger = Address::generate(&ctx.env); + + // Before a writer is set, a stranger is rejected with WriterNotSet. + assert_eq!( + ctx.client() + .try_record_fill(&stranger, &ctx.solver, &0), + Err(Ok(Error::WriterNotSet.into())) + ); + + ctx.client().set_writer(&writer); + assert_eq!(ctx.client().get_writer(), Some(writer.clone())); + + // Writer works… + ctx.client().record_fill(&writer, &ctx.solver, &(10 * USDC)); + assert_eq!(ctx.client().get_solver(&ctx.solver).unwrap().fills_completed, 1); + + // …a stranger still does not. + assert_eq!( + ctx.client() + .try_record_fill(&stranger, &ctx.solver, &0), + Err(Ok(Error::Unauthorized.into())) + ); +} + +// ─── Tier demotion on slash ──────────────────────────────────────────────── + +#[test] +fn slash_demotes_tier_and_pays_fee_recipient() { + let ctx = setup(); + // Bond exactly at the Bronze floor. + ctx.register(500 * USDC); + // One clean fill → score ~9_001 → qualifies for Bronze (needs >= 1_000). + ctx.client().record_fill(&ctx.admin, &ctx.solver, &(100 * USDC)); + assert_eq!(ctx.client().get_tier(&ctx.solver), 1); + + let (slashed, new_tier) = ctx.client().slash(&ctx.admin, &ctx.solver); + + // Bronze slash is the full 10% → 50 USDC, dropping bond to 450 USDC, + // below the 500 USDC Bronze floor → demoted to Unranked. + assert_eq!(slashed, 50 * USDC); + assert_eq!(new_tier, 0); + assert_eq!(ctx.client().get_tier(&ctx.solver), 0); + assert_eq!(ctx.bond().balance(&ctx.fee_recipient), 50 * USDC); + + let rec = ctx.client().get_solver(&ctx.solver).unwrap(); + assert_eq!(rec.bond_amount, 450 * USDC); + assert_eq!(rec.fills_failed, 1); + assert_eq!(rec.slashed_total, 50 * USDC); + assert!(rec.last_slash_time >= rec.registered_at); +} + +#[test] +fn slash_uses_the_tier_specific_bps() { + let ctx = setup(); + // Platinum: bond 50_000 USDC + a clean fill → score ~9_001 ≥ 9_000. + ctx.register(50_000 * USDC); + ctx.client().record_fill(&ctx.admin, &ctx.solver, &(100 * USDC)); + assert_eq!(ctx.client().get_tier(&ctx.solver), 4); + + // Platinum slash bps = 500 → 5% of 50_000 = 2_500 USDC. + let (slashed, _new_tier) = ctx.client().slash(&ctx.admin, &ctx.solver); + assert_eq!(slashed, 2_500 * USDC); +} + +// ─── Threshold tuning ────────────────────────────────────────────────────── + +#[test] +fn set_tier_threshold_changes_gating() { + let ctx = setup(); + let c = ctx.client(); + + // Raise Bronze to (600 USDC, 1_200 bps). + c.set_tier_threshold(&1, &(600 * USDC), &1_200); + let row = c.get_tier_table().get(1).unwrap(); + assert_eq!(row.min_bond, 600 * USDC); + assert_eq!(row.min_score_bps, 1_200); + + assert_eq!(c.tier_for(&1_200, &(600 * USDC)), 1); + assert_eq!(c.tier_for(&1_100, &(600 * USDC)), 0); // below the raised score bar + assert_eq!(c.tier_for(&1_200, &(599 * USDC)), 0); // below the raised bond bar +} + +#[test] +fn set_tier_threshold_rejects_tier_zero() { + let ctx = setup(); + let res = ctx.client().try_set_tier_threshold(&0, &(10 * USDC), &0); + assert_eq!(res, Err(Ok(Error::InvalidTier.into()))); +} + +#[test] +fn set_tier_threshold_rejects_out_of_bounds() { + let ctx = setup(); + let c = ctx.client(); + assert_eq!( + c.try_set_tier_threshold(&1, &(600 * USDC), &10_000), + Err(Ok(Error::ThresholdOutOfBounds.into())) + ); + assert_eq!( + c.try_set_tier_threshold(&1, &0, &1_200), + Err(Ok(Error::ThresholdOutOfBounds.into())) + ); + assert_eq!( + c.try_set_tier_threshold(&1, &(2_000_000 * USDC), &1_200), + Err(Ok(Error::ThresholdOutOfBounds.into())) + ); +} + +#[test] +fn set_tier_threshold_rejects_non_monotonic() { + let ctx = setup(); + let c = ctx.client(); + // Tier 2 dropping below tier 1's bond. + assert_eq!( + c.try_set_tier_threshold(&2, &(100 * USDC), &3_500), + Err(Ok(Error::ThresholdsNotMonotonic.into())) + ); + // Tier 1 rising to/above tier 2's score. + assert_eq!( + c.try_set_tier_threshold(&1, &(600 * USDC), &3_500), + Err(Ok(Error::ThresholdsNotMonotonic.into())) + ); +} + +// ─── Perk getters ───────────────────────────────────────────────────────── + +#[test] +fn perk_getters_match_table() { + let ctx = setup(); + let c = ctx.client(); + assert_eq!(c.get_slash_bps(&0), 1_000); + assert_eq!(c.get_slash_bps(&2), 800); + assert_eq!(c.get_slash_bps(&4), 500); + assert_eq!(c.get_fill_window_bonus_pct(&3), 30); + assert_eq!(c.get_fee_rebate_bps(&4), 0); + assert_eq!( + c.try_get_slash_bps(&5), + Err(Ok(Error::InvalidTier.into())) + ); +}