Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 18 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions contracts/compliance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ pub enum DataKey {
/// unset (`None`) for addresses tracked before this field existed. Purely metadata —
/// does not affect `is_allowed`.
Jurisdiction(Address),
/// Timestamp of the caller's last `bulk_allow_addresses` call, keyed per admin.
/// See [`BULK_OP_COOLDOWN_SECS`] and `check_bulk_op_cooldown` (#454).
LastBulkAllow(Address),
/// Timestamp of the caller's last `bulk_block_addresses` call, keyed per admin.
/// See [`BULK_OP_COOLDOWN_SECS`] and `check_bulk_op_cooldown` (#454).
LastBulkBlock(Address),
}

/// Coarse classification of an address's compliance state.
Expand Down
5 changes: 5 additions & 0 deletions contracts/settlement-workflow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ testutils = ["soroban-sdk/testutils"]
[dependencies]
soroban-sdk.workspace = true
compliance-client = { package = "comebackhere-compliance-client", path = "../../crates/compliance-client" }
# `multisig` holds no `#[contractimpl]` (only the shared `TreasuryError` enum and
# contract types), so depending on it directly does not statically link any
# foreign wasm exports into this contract — unlike the `compliance` / `treasury`
# impl crates, which stay dev-only for that reason.
multisig = { package = "comebackhere-multisig", path = "../../crates/multisig" }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Expand Down
21 changes: 9 additions & 12 deletions contracts/settlement-workflow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,20 @@ pub trait TreasuryInterface {
fn get_signer_weight(env: Env, signer: Address) -> u32;
}

/// Storage key for the ordered list of settlement IDs executed through this
/// workflow contract (as opposed to executed directly against treasury, bypassing
/// the compliance gate). See `get_executed_settlement_ids_page` (#373).
#[contracttype]
pub enum DataKey {
ExecutedSettlements,
}

/// Instance-storage keys for the workflow's pinned configuration. The compliance
/// and treasury instances are set once at initialization (#364) so the contract
/// enforces which instances it trusts rather than trusting whatever a caller
/// supplies per-call.
/// Storage keys for the workflow contract.
///
/// `ComplianceId` / `TreasuryId` pin the compliance and treasury instances this
/// workflow trusts; they are set once at initialization (#364) so the contract
/// enforces which instances it uses rather than trusting whatever a caller
/// supplies per-call. `ExecutedSettlements` is the ordered list of settlement
/// IDs executed through this (compliance-gated) workflow, as opposed to executed
/// directly against treasury — see `get_executed_settlement_ids_page` (#373).
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
ComplianceId,
TreasuryId,
ExecutedSettlements,
}

/// Reference on-chain implementation of the `SettlementWorkflow` role described in
Expand Down
68 changes: 65 additions & 3 deletions contracts/treasury/src/deposits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,72 @@ fn deposit_one(env: &Env, from: &Address, token_contract: &Address, amount: i128
balance = balance
.checked_add(amount)
.ok_or(TreasuryError::ArithmeticOverflow)?;
env.storage()
.persistent()
.set(&DataKey::Balance(from.clone()), &balance);
env.storage().persistent().set(
&DataKey::Balance(from.clone(), token_contract.clone()),
&balance,
);
env.events()
.publish((Symbol::new(env, "deposit"), from.clone()), amount);
Ok(())
}

/// Enforces the admin-configured rolling-window withdrawal cap (see
/// `set_withdrawal_limit` / `get_withdrawal_limit` in `lib.rs`). Tracked per
/// `addr` so `withdraw` (keyed on the recipient `to`) and `withdraw_all` (keyed
/// on `recipient`) each accumulate against their own window.
///
/// No-op when the limit is unset or `<= 0` (the default: uncapped). When a
/// limit is configured, the first withdrawal of a window records the window
/// start; subsequent withdrawals inside `WithdrawalWindowSecs` accumulate, and
/// a withdrawal that would push the window total past the limit panics with
/// `WithdrawalLimitExceeded` before any transfer happens.
pub(crate) fn enforce_withdrawal_limit(env: &Env, addr: &Address, amount: i128) {
let limit: i128 = env
.storage()
.instance()
.get(&DataKey::WithdrawalLimitPerWindow)
.unwrap_or(0);
if limit <= 0 {
return; // uncapped (default)
}
let window_secs: u64 = env
.storage()
.instance()
.get(&DataKey::WithdrawalWindowSecs)
.unwrap_or(0);
let now = env.ledger().timestamp();
let window_start: u64 = env
.storage()
.instance()
.get(&DataKey::WithdrawalWindowStart(addr.clone()))
.unwrap_or(0);
let used: i128 = env
.storage()
.instance()
.get(&DataKey::WithdrawnInWindow(addr.clone()))
.unwrap_or(0);

// Start a fresh window if the configured window has elapsed since it began
// (or if no window duration is configured, so every call is its own window).
let window_elapsed = window_secs == 0 || now.saturating_sub(window_start) >= window_secs;
let (current_start, prior_used) = if window_elapsed {
(now, 0i128)
} else {
(window_start, used)
};

let new_used = prior_used
.checked_add(amount)
.unwrap_or_else(|| soroban_sdk::panic_with_error!(env, TreasuryError::ArithmeticOverflow));
if new_used > limit {
soroban_sdk::panic_with_error!(env, TreasuryError::WithdrawalLimitExceeded);
}

env.storage().instance().set(
&DataKey::WithdrawalWindowStart(addr.clone()),
&current_start,
);
env.storage()
.instance()
.set(&DataKey::WithdrawnInWindow(addr.clone()), &new_used);
}
13 changes: 12 additions & 1 deletion contracts/treasury/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ impl TreasuryContract {
env.events()
.publish((Symbol::new(&env, "dispute_resolved"), dispute_id), dispute);
release_settlement_hold_if_no_open_disputes(&env, settlement_id);
Ok(())
}

/// Resolves an open dispute by splitting `dispute.amount` between claimant and
Expand Down Expand Up @@ -208,7 +209,17 @@ impl TreasuryContract {
if counterparty_amount > 0 {
token_client.transfer(&treasury, &dispute.counterparty, &counterparty_amount);
}
Ok(())
dispute.status = DisputeStatus::ResolvedSplit;
dispute.claimant_share_bps = claimant_bps;
let settlement_id = dispute.settlement_id;
env.storage()
.persistent()
.set(&DataKey::Dispute(dispute_id), &dispute);
env.events().publish(
(Symbol::new(&env, "dispute_resolved_split"), dispute_id),
dispute,
);
release_settlement_hold_if_no_open_disputes(&env, settlement_id);
}

/// Casts a weighted signer vote on a dispute; auto-resolves when cumulative weight meets threshold.
Expand Down
Loading