Skip to content
Open
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
21 changes: 21 additions & 0 deletions app/contract/contracts/Folder/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,27 @@ pub fn migrate(env: &Env, caller: &Address) -> Result<u32, RustAcademyError> {
fn migrate_legacy_to_v1(env: &Env) -> u32 {
storage::set_contract_version(env, storage::CURRENT_CONTRACT_VERSION);
storage::set_initialized(env, true);

// Migrate FeeConfig schema version if it exists
let key = storage::DataKey::FeeConfig;
if let Some(mut fee_cfg) = env.storage().persistent().get(&key) {
storage::migrate_fee_config(&mut fee_cfg);
env.storage().persistent().set(&key, &fee_cfg);
storage::set_or_extend_ttl(env, &key, storage::RecordType::FeeConfig);
}

// Migrate OracleFeeConfig schema version if it exists
let key = storage::DataKey::OracleFeeConfig;
if let Some(mut oracle_cfg) = env.storage().persistent().get(&key) {
storage::migrate_oracle_fee_config(&mut oracle_cfg);
env.storage().persistent().set(&key, &oracle_cfg);
storage::set_or_extend_ttl(env, &key, storage::RecordType::FeeConfig);
}

// Note: EscrowEntry and StealthEscrowEntry records are migrated on-read
// via the schema_version field check in get_escrow/get_stealth_escrow.
// PerAssetFeeConfig records are migrated on-write via set_per_asset_fee.

storage::CURRENT_CONTRACT_VERSION
}

Expand Down
54 changes: 23 additions & 31 deletions app/contract/contracts/Folder/src/escrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,9 @@ use crate::{
errors:: RustAcademyError,
escrow_id, events, fee_router, hook,
storage::{
count_dispute_votes, get_commitment_escrow_id, get_dispute_vote, get_escrow,
get_escrow_id_mapping, has_dispute_vote, has_escrow, put_commitment_escrow_id,
put_dispute_vote, put_escrow, put_escrow_id_mapping, remove_commitment_escrow_id,
remove_dispute_vote, remove_escrow, remove_escrow_id_mapping, LEDGER_THRESHOLD,
SIX_MONTHS_IN_LEDGERS,
count_dispute_votes, get_dispute_vote, get_escrow, get_escrow_id_mapping, has_dispute_vote,
has_escrow, put_dispute_vote, put_escrow, put_escrow_id_mapping, remove_dispute_votes_for_escrow,
remove_escrow, DataKey, LEDGER_THRESHOLD, SIX_MONTHS_IN_LEDGERS,
},
types::{DisputeVote, EscrowEntry, EscrowStatus, HookEventKind, Role},
};
Expand Down Expand Up @@ -199,6 +197,7 @@ pub fn deposit(
arbiter,
arbiters: Vec::new(env),
arbiter_threshold: 0,
schema_version: crate::types::ESCROW_SCHEMA_VERSION,
};

put_escrow(env, &commitment_bytes, &entry);
Expand Down Expand Up @@ -286,6 +285,7 @@ pub fn deposit_with_commitment(
arbiter,
arbiters: Vec::new(env),
arbiter_threshold: 0,
schema_version: crate::types::ESCROW_SCHEMA_VERSION,
};

put_escrow(env, &commitment_bytes, &entry);
Expand Down Expand Up @@ -368,6 +368,7 @@ pub fn deposit_partial(
arbiter,
arbiters: Vec::new(env),
arbiter_threshold: 0,
schema_version: crate::types::ESCROW_SCHEMA_VERSION,
};

put_escrow(env, &commitment_bytes, &entry);
Expand Down Expand Up @@ -683,43 +684,34 @@ pub fn extend_escrow_ttl(env: &Env, commitment: BytesN<32>) -> Result<(), RustA

/// Cleanup terminal escrow entries to reclaim storage deposits.
///
/// Only escrows in `Spent` or `Refunded` status can be removed. In addition to
/// the primary record, this removes every auxiliary index that referenced the
/// escrow so no stale lookup can resolve to a removed entry (Issue #51):
///
/// - the `escrow_id → commitment` dedup mapping and its reverse index, and
/// - any per-arbiter dispute votes recorded for the commitment.
/// Only escrows in `Spent` or `Refunded` status can be removed.
/// Also removes the associated EscrowIdMap and any dispute votes
/// for Disputed escrows that were resolved before cleanup.
///
/// All cleanup is bounded: index removals are O(1) and dispute-vote removal is
/// O(number of arbiters on the escrow). No path iterates global contract state.
/// Issue #19: Bounded cleanup ensures no orphaned mappings remain.
pub fn cleanup_escrow(env: &Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> {
let commitment_bytes: Bytes = commitment.clone().into();
let entry: EscrowEntry =
get_escrow(env, &commitment_bytes).ok_or( RustAcademyError::CommitmentNotFound)?;

match entry.status {
EscrowStatus::Spent | EscrowStatus::Refunded => {
// Primary record first.
remove_escrow(env, &commitment_bytes);

let mut indices_removed: u32 = 0;

// Dedup mapping (escrow_id → commitment) plus its reverse index.
if let Some(escrow_id) = get_commitment_escrow_id(env, &commitment_bytes) {
remove_escrow_id_mapping(env, &escrow_id);
remove_commitment_escrow_id(env, &commitment_bytes);
indices_removed += 2;
// Remove dispute votes if this was a disputed escrow that was resolved.
if matches!(entry.status, EscrowStatus::Refunded) && entry.arbiter.is_some() {
// Single arbiter mode - remove the vote if it exists
let arbiter = entry.arbiter.unwrap();
let key = DataKey::DisputeVote(commitment_bytes.clone(), arbiter);
env.storage().persistent().remove(&key);
} else if entry.arbiter_threshold > 0 {
// Multi-sig mode - remove all votes for this escrow
remove_dispute_votes_for_escrow(env, &commitment_bytes, &entry.arbiters);
}

// Per-arbiter dispute votes (bounded by the escrow's arbiter set).
for arbiter in entry.arbiters.iter() {
if has_dispute_vote(env, &commitment_bytes, &arbiter) {
remove_dispute_vote(env, &commitment_bytes, &arbiter);
indices_removed += 1;
}
}
remove_escrow(env, &commitment_bytes);

// Publish cleanup event for indexers
events::publish_escrow_cleanup(env, commitment);

events::publish_aux_indices_cleaned(env, commitment, indices_removed);
Ok(())
}
_ => Err( RustAcademyError::AlreadySpent), // Reuse error or add a more specific one if needed
Expand Down
23 changes: 23 additions & 0 deletions app/contract/contracts/Folder/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,3 +1025,26 @@ pub(crate) fn publish_per_asset_fee_set(
}
.publish(env);
}

// -----------------------------------------------------------------------------
// Escrow Cleanup Event (Issue #19)
// -----------------------------------------------------------------------------

#[contractevent(topics = ["TOPIC_ESCROW", "EscrowCleanup"])]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EscrowCleanupEvent {
#[topic]
pub escrow_id: BytesN<32>,

pub schema_version: u32,
pub timestamp: u64,
}

pub(crate) fn publish_escrow_cleanup(env: &Env, commitment: BytesN<32>) {
EscrowCleanupEvent {
escrow_id: commitment,
schema_version: EVENT_SCHEMA_VERSION,
timestamp: env.ledger().timestamp(),
}
.publish(env);
}
7 changes: 6 additions & 1 deletion app/contract/contracts/Folder/src/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ pub fn get_oracle_fee_config(env: &Env) -> Option<OracleFeeConfig> {
storage::get_oracle_fee_config(env)
}

/// Fetch the current price and timestamp from an external oracle contract.
///
/// Returns `Some((price_micros, timestamp))` when the oracle is available
/// and the data is fresh, or `None` if the oracle contract has not been
/// deployed or the call fails. Callers should fall back to static fee
/// configuration when this returns `None`.
pub fn fetch_price(_env: &Env, _oracle: &Address) -> Option<(i128, u64)> {
// TODO: Implement oracle price fetch once oracle contract is defined
None
}
1 change: 1 addition & 0 deletions app/contract/contracts/Folder/src/stealth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub fn register_ephemeral_key(
status: EscrowStatus::Pending,
created_at: now,
expires_at,
schema_version: crate::types::STEALTH_ESCROW_SCHEMA_VERSION,
};

put_stealth_escrow(env, &stealth_address, &entry);
Expand Down
Loading
Loading