Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3b5f3a9
fix(escrow): implement explicit legacy version and migration checks
onyekachi66 Aug 30, 2026
7b9d9ea
fix(escrow): remove conflicting tests.rs in favor of tests/mod.rs
onyekachi66 Sep 1, 2026
c9e16eb
style: fix cargo fmt issues in lib.rs and release_budget_tests.rs
onyekachi66 Sep 1, 2026
68206a2
style: fix checked_add format and remove blank line in test file
onyekachi66 Sep 1, 2026
4d8cdc5
test: expect EscrowNotInitialized on uninitialized migrate call
onyekachi66 Sep 1, 2026
24222f2
fix(tests): align migration tests with expected nonce parameter and l…
onyekachi66 Sep 1, 2026
b36b451
ci: fix invalid rust wasm target in workflow
onyekachi66 Sep 1, 2026
84d0315
ci: fix rust-toolchain action inputs and test verbosity
onyekachi66 Sep 1, 2026
f235c7c
fix(tests): import LEGACY_VERSION in migration_errors.rs
onyekachi66 Sep 1, 2026
1883306
fix(tests): correct expected nonces in sequential reverted migration …
onyekachi66 Sep 4, 2026
803a899
fix(tests): correct expected error for mismatched version claim
onyekachi66 Sep 4, 2026
4d1e6a5
fix(escrow): format codebase with cargo fmt and align migration nonces
onyekachi66 Sep 4, 2026
6aaa498
fix(tests): align expected nonces with Soroban rollback on error
onyekachi66 Sep 4, 2026
5200b80
fix(escrow): simplify storage lookup in migrate to satisfy clippy
onyekachi66 Sep 4, 2026
b86268b
fix(escrow): align migrate flow with runbook pattern and clippy
onyekachi66 Sep 4, 2026
9b5204f
ci: log clippy and test diagnostics to step summary
onyekachi66 Sep 4, 2026
91164f0
ci: capture cargo build diagnostics to step summary
onyekachi66 Sep 4, 2026
79a72b2
ci: capture build wasm diagnostics to step summary
onyekachi66 Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: rustfmt, clippy, llvm-tools-preview
targets: wasm32v1-none
targets: wasm32-unknown-unknown

- name: Cache cargo registry and build
uses: Swatinem/rust-cache@v2
Expand All @@ -25,21 +26,26 @@ jobs:
run: cargo fmt -p liquifact_escrow -- --check

- name: Clippy
run: cargo clippy -p liquifact_escrow -- -D warnings
run: |
cargo clippy -p liquifact_escrow -- -D warnings 2>&1 | tee -a $GITHUB_STEP_SUMMARY

- name: Build
run: cargo build
run: |
cargo build 2>&1 | tee -a $GITHUB_STEP_SUMMARY

- name: Run tests
run: cargo test
run: |
export RUST_BACKTRACE=1
cargo test -p liquifact_escrow -- --nocapture 2>&1 | tee -a $GITHUB_STEP_SUMMARY

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov

- name: Add WASM target
run: rustup target add wasm32v1-none
run: rustup target add wasm32-unknown-unknown
- name: Build WASM
run: cargo build --target wasm32v1-none --release -p liquifact_escrow
run: |
cargo build --target wasm32-unknown-unknown --release -p liquifact_escrow 2>&1 | tee -a $GITHUB_STEP_SUMMARY

- name: Workspace Clippy (all targets)
# Latent clippy denies in tests; keep signal without hard-failing CI.
Expand Down
86 changes: 68 additions & 18 deletions escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ pub const MAX_INVESTOR_ALLOWLIST_BATCH: u32 = 32;
///
/// See `docs/OPERATOR_RUNBOOK.md` for the full redeploy-vs-upgrade decision tree.
pub const SCHEMA_VERSION: u32 = 6;

/// Explicit legacy version before version markers were introduced.
pub const LEGACY_VERSION: u32 = 5;
// See the schema version contract documentation: [Escrow schema versioning](../docs/escrow-schema-versioning.md)

/// Version of the lifecycle event topics emitted by this contract.
Expand Down Expand Up @@ -282,7 +285,12 @@ impl LiquifactEscrow {
panic_with_error!(&env, CloseError::ActiveBalance);
}

if env.storage().instance().get(&DataKey::Dispute).unwrap_or(false) {
if env
.storage()
.instance()
.get(&DataKey::Dispute)
.unwrap_or(false)
{
panic_with_error!(&env, CloseError::ActiveDispute);
}

Expand Down Expand Up @@ -315,7 +323,10 @@ impl LiquifactEscrow {

/// Toggles the dispute active flag. Bumps TTL by the disputed threshold.
pub fn set_dispute_active(env: Env, active: bool) {
let mut escrow: InvoiceEscrow = env.storage().instance().get(&DataKey::Escrow)
let mut escrow: InvoiceEscrow = env
.storage()
.instance()
.get(&DataKey::Escrow)
.unwrap_or_else(|| panic_with_error!(&env, CloseError::NotInitialized));
escrow.admin.require_auth();
escrow.dispute_active = active;
Expand Down Expand Up @@ -505,7 +516,11 @@ pub(crate) fn get_lifecycle_ttl(escrow: &InvoiceEscrow) -> u32 {
}
}

pub(crate) fn extend_ttl_for_activity(env: &Env, escrow: &InvoiceEscrow, investor: Option<Address>) {
pub(crate) fn extend_ttl_for_activity(
env: &Env,
escrow: &InvoiceEscrow,
investor: Option<Address>,
) {
let ttl = get_lifecycle_ttl(escrow);
env.storage().instance().extend_ttl(ttl, ttl);
if let Some(addr) = investor {
Expand Down Expand Up @@ -724,6 +739,8 @@ pub enum EscrowError {
AlreadyCurrentSchemaVersion = 91,
/// [`LiquifactEscrow::migrate`] has no implemented path from the requested version.
NoMigrationPath = 92,
/// Storage lacks a version marker and does not match the known legacy layout.
AmbiguousLegacyStorage = 93,

/// [`LiquifactEscrow::fund`] / [`LiquifactEscrow::fund_with_commitment`] received non-positive amount.
FundingAmountNotPositive = 100,
Expand Down Expand Up @@ -3612,7 +3629,11 @@ impl LiquifactEscrow {
/// | Legal hold active | [`EscrowError::LegalHoldBlocksBeneficiaryRotation`] |
/// | Escrow not open or funded | [`EscrowError::RotationNotOpen`] |
/// | `new_sme_address == current SME` | [`EscrowError::NewSmeSameAsCurrent`] |
pub fn rotate_beneficiary(env: Env, new_sme_address: Address, expected_nonce: u32) -> InvoiceEscrow {
pub fn rotate_beneficiary(
env: Env,
new_sme_address: Address,
expected_nonce: u32,
) -> InvoiceEscrow {
// Legal-hold gate (read-only).
guard_not_legal_hold(&env, EscrowError::LegalHoldBlocksBeneficiaryRotation);

Expand Down Expand Up @@ -3780,10 +3801,14 @@ impl LiquifactEscrow {
.instance()
.get(&DataKey::AdminNonce)
.unwrap_or(0);
ensure(env, current == expected_nonce, EscrowError::AdminNonceMismatch);
let next = current.checked_add(1).unwrap_or_else(|| {
fail(env, EscrowError::AdminNonceMismatch)
});
ensure(
env,
current == expected_nonce,
EscrowError::AdminNonceMismatch,
);
let next = current
.checked_add(1)
.unwrap_or_else(|| fail(env, EscrowError::AdminNonceMismatch));
env.storage().instance().set(&DataKey::AdminNonce, &next);
}

Expand Down Expand Up @@ -5672,7 +5697,12 @@ impl LiquifactEscrow {
/// - [`LiquifactEscrow::is_investor_allowlisted`] — check if an address is allowlisted
/// - [`LiquifactEscrow::set_investors_allowlisted`] — batch variant for multiple addresses
/// - [`docs/escrow-allowlist.md`](../docs/escrow-allowlist.md) — full allowlist model documentation
pub fn set_investor_allowlisted(env: Env, investor: Address, allowed: bool, expected_nonce: u32) {
pub fn set_investor_allowlisted(
env: Env,
investor: Address,
allowed: bool,
expected_nonce: u32,
) {
let escrow = Self::load_escrow_require_admin(&env);
Self::consume_admin_nonce(&env, expected_nonce);
env.storage()
Expand Down Expand Up @@ -5728,7 +5758,12 @@ impl LiquifactEscrow {
/// - [`LiquifactEscrow::set_investor_allowlisted`] — single-address variant
/// - [`LiquifactEscrow::is_investor_allowlisted`] — check if an address is allowlisted
/// - [`docs/escrow-allowlist.md`](../docs/escrow-allowlist.md) — full allowlist model documentation
pub fn set_investors_allowlisted(env: Env, investors: Vec<Address>, allowed: bool, expected_nonce: u32) {
pub fn set_investors_allowlisted(
env: Env,
investors: Vec<Address>,
allowed: bool,
expected_nonce: u32,
) {
let escrow = Self::load_escrow_require_admin(&env);
Self::consume_admin_nonce(&env, expected_nonce);

Expand Down Expand Up @@ -6213,7 +6248,19 @@ impl LiquifactEscrow {
Self::load_escrow_require_admin(&env);
Self::consume_admin_nonce(&env, expected_nonce);

let stored: u32 = env.storage().instance().get(&DataKey::Version).unwrap_or(0);
let stored: u32 = env
.storage()
.instance()
.get(&DataKey::Version)
.unwrap_or_else(|| {
let has_token = env.storage().instance().has(&DataKey::FundingToken);
let has_treasury = env.storage().instance().has(&DataKey::Treasury);
if has_token && has_treasury {
LEGACY_VERSION
} else {
fail(&env, EscrowError::AmbiguousLegacyStorage)
}
});

ensure(
&env,
Expand All @@ -6222,14 +6269,17 @@ impl LiquifactEscrow {
);

if from_version >= SCHEMA_VERSION {
fail(&env, EscrowError::AlreadyCurrentSchemaVersion)
} else {
// No migration path is implemented for any version below SCHEMA_VERSION.
// To add one: implement the transformation here, call
// env.storage().instance().set(&DataKey::Version, &NEW_VERSION);
// and return NEW_VERSION before reaching this typed error.
fail(&env, EscrowError::NoMigrationPath)
fail(&env, EscrowError::AlreadyCurrentSchemaVersion);
}

if from_version == LEGACY_VERSION {
env.storage()
.instance()
.set(&DataKey::Version, &SCHEMA_VERSION);
return SCHEMA_VERSION;
}

fail(&env, EscrowError::NoMigrationPath)
}

/// Replaces the deployed WASM bytecode for this contract instance while preserving all
Expand Down
5 changes: 2 additions & 3 deletions escrow/src/release_budget_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
use soroban_sdk::{testutils::Address as _, Address, Env, String, Symbol, Vec};

use super::{
keys, LiquifactEscrow, LiquifactEscrowClient, MAX_INVESTOR_READ_BATCH,
MAX_UNIQUE_INVESTORS, WORST_CASE_RELEASE_CPU_INSNS_CEILING,
WORST_CASE_RELEASE_MEM_BYTES_CEILING,
keys, LiquifactEscrow, LiquifactEscrowClient, MAX_INVESTOR_READ_BATCH, MAX_UNIQUE_INVESTORS,
WORST_CASE_RELEASE_CPU_INSNS_CEILING, WORST_CASE_RELEASE_MEM_BYTES_CEILING,
};

/// Tally of measured CPU/memory for a single top-level release-path invocation.
Expand Down
Loading
Loading