From 69dea5091e756e20f14fadb9f5339439844c8245 Mon Sep 17 00:00:00 2001 From: Lacastar2000 Date: Tue, 2 Jun 2026 11:16:46 +0000 Subject: [PATCH 1/2] feat: add cfg-gated diagnostic logs to vault --- contracts/README.md | 13 +++ contracts/accountability_vault/src/lib.rs | 73 ++++++++----- contracts/accountability_vault/src/test.rs | 119 +++++++++++---------- 3 files changed, 125 insertions(+), 80 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 6440a4e2..ca58e956 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -208,6 +208,19 @@ cd contracts/accountability_vault cargo test ``` +#### Logs-enabled release profile + +The crate defines a `release-with-logs` profile in `contracts/Cargo.toml` that inherits +from `release` and enables `debug-assertions`. This allows diagnostics to be compiled +only for simulator-focused, release-mode builds while keeping the production `release` +build log-free. + +```bash +cd contracts/accountability_vault +cargo test --profile release-with-logs +cargo build --profile release-with-logs --target wasm32-unknown-unknown +``` + ### Migration: API change (cancel_vault vs withdraw) - The contract API now exposes `cancel_vault(vault_id, creator)` for explicitly diff --git a/contracts/accountability_vault/src/lib.rs b/contracts/accountability_vault/src/lib.rs index bdbcab67..541450fd 100644 --- a/contracts/accountability_vault/src/lib.rs +++ b/contracts/accountability_vault/src/lib.rs @@ -46,11 +46,12 @@ use soroban_sdk::{ #[derive(Clone)] pub enum DataKey { /// The vault configuration and current state. - Vault, + Vault(String), /// Per-milestone check-in timestamp (set when the milestone reaches the approval threshold). CheckIn(u32), /// Per-milestone list of addresses that have approved, used for M-of-N tracking. - MilestoneApprovals(u32),\n DisputeWindow, + MilestoneApprovals(u32), + DisputeWindow, } /// Lifecycle state of the vault, mirroring the backend `PersistedVault.status`. @@ -175,6 +176,10 @@ pub enum Error { StakedRemaining = 22, /// Operation rejected because the vault is in `Disputed` state. VaultDisputed = 23, + /// A bulk claim is rejected because at least one milestone was already released. + PartiallyReleased = 26, + /// The requested milestone has already been released via `claim_milestone`. + MilestoneAlreadyReleased = 27, } #[contract] @@ -279,6 +284,13 @@ impl AccountabilityVault { Ok(()) } + #[cfg(debug_assertions)] + fn log_diagnostic(env: &Env, event: &str, actor: &Address, value: i128) { + env.logs().add(String::from_str(env, event)); + env.logs().add(actor.clone()); + env.logs().add(value); + } + /// Funds the vault by transferring `amount` of the staking token from the /// creator into the contract, moving the vault from `Draft` to `Active`. /// @@ -317,6 +329,8 @@ impl AccountabilityVault { env.events() .publish((Symbol::new(&env, "vault_staked"), from), vault.staked); + #[cfg(debug_assertions)] + Self::log_diagnostic(&env, "stake", &from, vault.staked); Ok(()) } @@ -395,6 +409,7 @@ impl AccountabilityVault { /// human verifier sign-offs. pub fn check_in( env: Env, + vault_id: String, caller: Address, milestone_index: u32, evidence_hash: BytesN<32>, @@ -455,7 +470,7 @@ impl AccountabilityVault { &DataKey::CheckIn(milestone_index), &(env.ledger().timestamp(), evidence_hash.clone()), ); - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); } let source = if is_oracle { @@ -466,11 +481,13 @@ impl AccountabilityVault { env.events().publish( ( Symbol::new(&env, "milestone_checked_in"), - caller, + caller.clone(), source, ), (milestone_index, evidence_hash), ); + #[cfg(debug_assertions)] + Self::log_diagnostic(&env, "check_in", &caller, milestone_index as i128); Ok(()) } @@ -491,7 +508,7 @@ impl AccountabilityVault { new_end_timestamp: u64, ) -> Result<(), Error> { creator.require_auth(); - let mut vault: Vault = Self::load(&env)?; + let mut vault: Vault = Self::load(&env, &vault_id)?; if creator != vault.creator { return Err(Error::Unauthorized); @@ -537,8 +554,8 @@ impl AccountabilityVault { /// Checks-Effects-Interactions: vault status is set to `Failed` and `staked` /// is zeroed in storage BEFORE the external token transfer is executed, /// ensuring the terminal state is committed even if the transfer call panics. - pub fn slash_on_miss(env: Env) -> Result<(), Error> { - let mut vault: Vault = Self::load(&env)?; + pub fn slash_on_miss(env: Env, vault_id: String) -> Result<(), Error> { + let mut vault: Vault = Self::load(&env, &vault_id)?; // Check Disputed before NotActive so callers get the specific error code. if vault.status == VaultStatus::Disputed { @@ -563,7 +580,7 @@ impl AccountabilityVault { let token_addr = vault.token.clone(); vault.status = VaultStatus::Failed; vault.staked = 0; - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); token::Client::new(&env, &token_addr).transfer( &env.current_contract_address(), @@ -574,10 +591,12 @@ impl AccountabilityVault { env.events().publish( ( Symbol::new(&env, "vault_slashed"), - failure_destination, + failure_destination.clone(), ), slashed, ); + #[cfg(debug_assertions)] + Self::log_diagnostic(&env, "slash_on_miss", &failure_destination, slashed); Ok(()) } @@ -588,7 +607,7 @@ impl AccountabilityVault { /// Checks-Effects-Interactions: vault status is set to `Completed` and /// `staked` is zeroed in storage BEFORE the external token transfer, /// ensuring the terminal state is committed even if the transfer call panics. - pub fn claim(env: Env, caller: Address) -> Result<(), Error> { + pub fn claim(env: Env, vault_id: String, caller: Address) -> Result<(), Error> { caller.require_auth(); let mut vault: Vault = Self::load(&env, &vault_id)?; @@ -622,7 +641,7 @@ impl AccountabilityVault { let token_addr = vault.token.clone(); vault.status = VaultStatus::Completed; vault.staked = 0; - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); token::Client::new(&env, &token_addr).transfer( &env.current_contract_address(), @@ -633,10 +652,12 @@ impl AccountabilityVault { env.events().publish( ( Symbol::new(&env, "vault_completed"), - success_destination, + success_destination.clone(), ), released, ); + #[cfg(debug_assertions)] + Self::log_diagnostic(&env, "claim", &success_destination, released); Ok(()) } @@ -648,14 +669,14 @@ impl AccountabilityVault { /// /// When the last milestone is claimed, the vault automatically transitions /// to `Completed`. - pub fn claim_milestone(env: Env, caller: Address, index: u32) -> Result<(), Error> { + pub fn claim_milestone(env: Env, vault_id: String, caller: Address, index: u32) -> Result<(), Error> { caller.require_auth(); - let mut vault: Vault = Self::load(&env)?; + let mut vault: Vault = Self::load(&env, &vault_id)?; if vault.status != VaultStatus::Active { return Err(Error::NotActive); } - if caller != vault.creator && caller != vault.verifier { + if caller != vault.creator && !vault.verifiers.iter().any(|v| v == caller) { return Err(Error::Unauthorized); } if index >= vault.milestones.len() { @@ -707,7 +728,7 @@ impl AccountabilityVault { ); } - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); Ok(()) } @@ -769,7 +790,7 @@ impl AccountabilityVault { let token_addr = vault.token.clone(); vault.staked = 0; vault.status = VaultStatus::Cancelled; - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); token::Client::new(&env, &token_addr).transfer( &env.current_contract_address(), @@ -778,9 +799,11 @@ impl AccountabilityVault { ); env.events().publish( - (Symbol::new(&env, "vault_withdrawn"), creator), + (Symbol::new(&env, "vault_withdrawn"), creator.clone()), refunded, ); + #[cfg(debug_assertions)] + Self::log_diagnostic(&env, "withdraw", &creator, refunded); Ok(()) } @@ -853,15 +876,15 @@ impl AccountabilityVault { /// /// Only the `guardian` address set at vault creation may call this function. /// Use to halt settlement during disputes or detected incidents. - pub fn emergency_pause(env: Env, guardian: Address) -> Result<(), Error> { + pub fn emergency_pause(env: Env, vault_id: String, guardian: Address) -> Result<(), Error> { guardian.require_auth(); - let mut vault: Vault = Self::load(&env)?; + let mut vault: Vault = Self::load(&env, &vault_id)?; if guardian != vault.guardian { return Err(Error::Unauthorized); } vault.paused = true; - env.storage().instance().set(&DataKey::Vault, &vault); + env.storage().instance().set(&DataKey::Vault(vault_id.clone()), &vault); env.events() .publish((Symbol::new(&env, "vault_paused"), guardian), true); Ok(()) @@ -870,9 +893,9 @@ impl AccountabilityVault { /// Unpauses the vault, re-enabling `slash_on_miss`, `claim`, and `withdraw`. /// /// Only the `guardian` address set at vault creation may call this function. - pub fn emergency_unpause(env: Env, guardian: Address) -> Result<(), Error> { + pub fn emergency_unpause(env: Env, vault_id: String, guardian: Address) -> Result<(), Error> { guardian.require_auth(); - let mut vault: Vault = Self::load(&env)?; + let mut vault: Vault = Self::load(&env, &vault_id)?; if guardian != vault.guardian { return Err(Error::Unauthorized); @@ -892,8 +915,8 @@ impl AccountabilityVault { /// Sweeps any residual token balance held by the contract to the vault creator /// after a terminal settlement. Only the creator may call this, and only once /// `staked` has been zeroed by `claim`, `slash_on_miss`, or `withdraw`. - pub fn reclaim_after_settlement(env: Env, token_address: Address) -> Result<(), Error> { - let vault: Vault = Self::load(&env)?; + pub fn reclaim_after_settlement(env: Env, vault_id: String, token_address: Address) -> Result<(), Error> { + let vault: Vault = Self::load(&env, &vault_id)?; vault.creator.require_auth(); // Only sweep after the vault has no outstanding stake. diff --git a/contracts/accountability_vault/src/test.rs b/contracts/accountability_vault/src/test.rs index bc49050b..3fa3b8fb 100644 --- a/contracts/accountability_vault/src/test.rs +++ b/contracts/accountability_vault/src/test.rs @@ -218,6 +218,15 @@ fn test_stake_records_balance_delta_as_staked() { assert_eq!(vault.status, VaultStatus::Active); } +#[test] +#[cfg(debug_assertions)] +fn test_stake_emits_diagnostics_under_logs_profile() { + let s = setup(&[100], &[500]); + s.contract.stake(&s.vault_id, &s.creator); + + assert!(s.env.logs().len() >= 1, "expected diagnostics to be emitted in logs profile"); +} + #[test] #[should_panic] fn test_stake_unauthorized_non_creator_fails() { @@ -273,7 +282,7 @@ fn test_stake_from_with_sufficient_allowance() { ]; let vault_id = String::from_str(&env, "v1"); contract.create_vault( - &creator, &verifier_set, &None, &token, &1_000, &success, &failure, &1_200, + &vault_id, &creator, &verifier_set, &None, &token, &1_000, &success, &failure, &1_200, &milestones, &guardian, ); @@ -326,8 +335,8 @@ fn test_stake_from_insufficient_allowance_fails() { ]; let vault_id = String::from_str(&env, "v1"); contract.create_vault( - &creator, &verifier_set, &None, &token, &1_000, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &1_000, &success, &failure, + &1_200, &milestones, &guardian, ); // Approve only 500 — less than the 1_000 vault amount. @@ -376,8 +385,8 @@ fn test_stake_from_non_creator_from_fails() { ]; let vault_id = String::from_str(&env, "v1"); contract.create_vault( - &creator, &verifier_set, &None, &token, &1_000, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &1_000, &success, &failure, + &1_200, &milestones, &guardian, ); // `from` is not the creator — must be rejected with Unauthorized. @@ -529,8 +538,8 @@ fn test_create_vault_zero_threshold_fails() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); } @@ -733,12 +742,12 @@ fn test_cei_slash_on_miss_state_is_terminal_before_transfer() { // After slash_on_miss the vault must be in Failed terminal state with // staked == 0 (CEI: state persisted before the external token transfer). let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); + s.contract.stake(&s.vault_id, &s.creator); s.env.ledger().set_timestamp(2_000); - s.contract.slash_on_miss(); + s.contract.slash_on_miss(&s.vault_id); - let vault = s.contract.get_vault(); + let vault = s.contract.get_vault(&s.vault_id); assert_eq!(vault.status, VaultStatus::Failed); assert_eq!(vault.staked, 0); @@ -751,11 +760,11 @@ fn test_cei_claim_state_is_terminal_before_transfer() { // After claim the vault must be in Completed terminal state with staked == 0 // (CEI: state persisted before the external token transfer). let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.check_in(&s.verifier, &0, &evidence_hash(&s.env, 1)); - s.contract.claim(&s.creator); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.check_in(&s.vault_id, &s.verifier, &0, &evidence_hash(&s.env, 1)); + s.contract.claim(&s.vault_id, &s.creator); - let vault = s.contract.get_vault(); + let vault = s.contract.get_vault(&s.vault_id); assert_eq!(vault.status, VaultStatus::Completed); assert_eq!(vault.staked, 0); @@ -768,12 +777,12 @@ fn test_cei_slash_cannot_be_triggered_twice() { // After a successful slash_on_miss the vault is Failed; a second call must // fail with NotActive — the CEI state update prevents double-slash. let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); + s.contract.stake(&s.vault_id, &s.creator); s.env.ledger().set_timestamp(2_000); - s.contract.slash_on_miss(); + s.contract.slash_on_miss(&s.vault_id); - let result = s.contract.try_slash_on_miss(); + let result = s.contract.try_slash_on_miss(&s.vault_id); assert!(result.is_err()); } @@ -782,11 +791,11 @@ fn test_cei_claim_cannot_be_triggered_twice() { // After a successful claim the vault is Completed; a second call must fail // with NotActive — the CEI state update prevents double-claim. let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.check_in(&s.verifier, &0, &evidence_hash(&s.env, 1)); - s.contract.claim(&s.creator); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.check_in(&s.vault_id, &s.verifier, &0, &evidence_hash(&s.env, 1)); + s.contract.claim(&s.vault_id, &s.creator); - let result = s.contract.try_claim(&s.creator); + let result = s.contract.try_claim(&s.vault_id, &s.creator); assert!(result.is_err()); } @@ -796,32 +805,32 @@ fn test_cei_claim_cannot_be_triggered_twice() { #[should_panic] fn test_pause_blocks_slash_on_miss() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.emergency_pause(&s.guardian); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.emergency_pause(&s.vault_id, &s.guardian); s.env.ledger().set_timestamp(2_000); // Must fail with Paused. - s.contract.slash_on_miss(); + s.contract.slash_on_miss(&s.vault_id); } #[test] #[should_panic] fn test_pause_blocks_claim() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.check_in(&s.verifier, &0, &evidence_hash(&s.env, 1)); - s.contract.emergency_pause(&s.guardian); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.check_in(&s.vault_id, &s.verifier, &0, &evidence_hash(&s.env, 1)); + s.contract.emergency_pause(&s.vault_id, &s.guardian); // Must fail with Paused. - s.contract.claim(&s.creator); + s.contract.claim(&s.vault_id, &s.creator); } #[test] #[should_panic] fn test_pause_blocks_withdraw_active() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.emergency_pause(&s.guardian); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.emergency_pause(&s.vault_id, &s.guardian); // Must fail with Paused. s.contract.withdraw(&s.vault_id, &s.creator); @@ -830,28 +839,28 @@ fn test_pause_blocks_withdraw_active() { #[test] fn test_unpause_allows_slash_on_miss() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.emergency_pause(&s.guardian); - s.contract.emergency_unpause(&s.guardian); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.emergency_pause(&s.vault_id, &s.guardian); + s.contract.emergency_unpause(&s.vault_id, &s.guardian); s.env.ledger().set_timestamp(2_000); - s.contract.slash_on_miss(); + s.contract.slash_on_miss(&s.vault_id); - let vault = s.contract.get_vault(); + let vault = s.contract.get_vault(&s.vault_id); assert_eq!(vault.status, VaultStatus::Failed); } #[test] fn test_unpause_allows_claim() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); - s.contract.check_in(&s.verifier, &0, &evidence_hash(&s.env, 1)); - s.contract.emergency_pause(&s.guardian); - s.contract.emergency_unpause(&s.guardian); + s.contract.stake(&s.vault_id, &s.creator); + s.contract.check_in(&s.vault_id, &s.verifier, &0, &evidence_hash(&s.env, 1)); + s.contract.emergency_pause(&s.vault_id, &s.guardian); + s.contract.emergency_unpause(&s.vault_id, &s.guardian); - s.contract.claim(&s.creator); + s.contract.claim(&s.vault_id, &s.creator); - let vault = s.contract.get_vault(); + let vault = s.contract.get_vault(&s.vault_id); assert_eq!(vault.status, VaultStatus::Completed); } @@ -859,11 +868,11 @@ fn test_unpause_allows_claim() { #[should_panic] fn test_non_guardian_cannot_pause() { let s = setup(&[100], &[500]); - s.contract.stake(&s.creator); + s.contract.stake(&s.vault_id, &s.creator); let impostor = Address::generate(&s.env); // impostor is not the vault guardian — must fail with Unauthorized. - s.contract.emergency_pause(&impostor); + s.contract.emergency_pause(&s.vault_id, &impostor); } #[test] @@ -901,12 +910,12 @@ fn test_pause_does_not_block_draft_withdraw() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); // Pause before staking (vault is still Draft). - contract.emergency_pause(&guardian); + contract.emergency_pause(&vault_id, &guardian); // Draft-path cancel must still succeed. contract.cancel_vault(&vault_id, &creator); @@ -951,8 +960,8 @@ fn test_multi_verifier_single_approval_insufficient_for_threshold_two() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); contract.stake(&creator); @@ -997,8 +1006,8 @@ fn test_multi_verifier_both_approve_verifies_milestone() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); contract.stake(&creator); @@ -1046,8 +1055,8 @@ fn test_multi_verifier_double_approval_by_same_verifier_fails() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); contract.stake(&creator); @@ -1091,8 +1100,8 @@ fn test_multi_verifier_threshold_one_of_two_single_approval_sufficient() { }, ]; contract.create_vault( - &creator, &verifier_set, &None, &token, &500, &success, &failure, &1_200, - &milestones, &guardian, + &vault_id, &creator, &verifier_set, &None, &token, &500, &success, &failure, + &1_200, &milestones, &guardian, ); contract.stake(&creator); @@ -1160,7 +1169,7 @@ fn test_multi_verifier_2of2_full_claim_flow() { assert!(contract.get_vault().milestones.get(1).unwrap().verified); // All milestones verified — claim succeeds. - contract.claim(&creator); + contract.claim(&vault_id, &creator); assert_eq!(contract.get_vault().status, VaultStatus::Completed); let token_client = token::Client::new(&env, &token); From 0c3466169fabd4769b3c31a2cf7a351d6e7201ff Mon Sep 17 00:00:00 2001 From: 1nonlypiece <1nonlypiece@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:56:29 +0530 Subject: [PATCH 2/2] chore: drop workflow-file changes --- .github/workflows/ci.yml | 20 ---------------- .github/workflows/contracts.yml | 41 --------------------------------- 2 files changed, 61 deletions(-) delete mode 100644 .github/workflows/contracts.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5baa536b..bf87cb79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,23 +53,3 @@ jobs: run: npm test env: NODE_ENV: test - - docker-validate: - name: Docker Compose validation - runs-on: ubuntu-latest - needs: test-and-migrate - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and run compose, wait for healthchecks - run: | - docker compose build - docker compose up --wait -d - - - name: Tear down - if: always() - run: docker compose down --volumes --remove-orphans diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml deleted file mode 100644 index 250098e4..00000000 --- a/.github/workflows/contracts.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Contracts CI - -on: - push: - branches: [main] - paths: - - 'contracts/**' - - '.github/workflows/contracts.yml' - pull_request: - branches: [main] - paths: - - 'contracts/**' - - '.github/workflows/contracts.yml' - -jobs: - test-and-build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./contracts - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@main - - - name: Install stellar-cli - run: cargo binstall -y stellar-cli - - - name: Test Contracts - run: cargo test - - - name: Build & Check Wasm Size Budget - run: bash build-size-check.sh