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
16 changes: 14 additions & 2 deletions contracts/ephemeral_account/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ pub struct AccountExpired {
pub reserve_amount: i128,
}

/// Reserve reclaimed event - emitted when base reserve is reclaimed
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReserveReclaimed {
pub destination: Address,
pub amount: i128,
pub sweep_id: u64,
pub fully_reclaimed: bool,
pub remaining_reserve: i128,
}

pub fn emit_account_created(env: &Env, creator: Address, expiry_ledger: u32) {
Expand Down Expand Up @@ -85,10 +87,20 @@ pub fn emit_account_expired(
env.events().publish((symbol_short!("expired"),), event);
}

pub fn emit_reserve_reclaimed(env: &Env, destination: Address, amount: i128) {
pub fn emit_reserve_reclaimed(
env: &Env,
destination: Address,
amount: i128,
sweep_id: u64,
fully_reclaimed: bool,
remaining_reserve: i128,
) {
let event = ReserveReclaimed {
destination,
amount,
sweep_id,
fully_reclaimed,
remaining_reserve,
};
env.events().publish((symbol_short!("reserve"),), event);
}
173 changes: 156 additions & 17 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub use events::{
};
pub use storage::DataKey;

const BASE_RESERVE_STROOPS: i128 = 1_000_000_000;

#[contract]
pub struct EphemeralAccountContract;

Expand Down Expand Up @@ -56,6 +58,7 @@ impl EphemeralAccountContract {
storage::set_expiry_ledger(&env, expiry_ledger);
storage::set_recovery_address(&env, &recovery_address);
storage::set_status(&env, AccountStatus::Active);
storage::init_reserve_tracking(&env, BASE_RESERVE_STROOPS);

// Emit event
events::emit_account_created(&env, creator, expiry_ledger);
Expand Down Expand Up @@ -167,17 +170,16 @@ impl EphemeralAccountContract {
storage::set_status(&env, AccountStatus::Swept);
storage::set_swept_to(&env, &destination);

// Note: Actual token transfers happen in the SDK via Stellar SDK
// This contract enforces the business logic and authorization
// The SDK will call this function, get approval, then execute all transfers atomically
// All transfers must succeed or the entire operation fails

// Base reserve amount (1 XLM in stroops)
let reserve_amount = 1_000_000_000i128;
// Note: Actual token transfers happen in the SDK via Stellar SDK.
// This contract enforces authorization/state transitions and reserve lifecycle.
let sweep_id = env.ledger().sequence() as u64;
storage::set_last_sweep_id(&env, sweep_id);

// Emit events for sweep execution and reserve reclamation
// Emit sweep event once transfer authorization/state update succeeds.
events::emit_sweep_executed_multi(&env, destination.clone(), &payments_vec);
events::emit_reserve_reclaimed(&env, destination, reserve_amount);

// Reclaim base reserve only after successful sweep state transition.
Self::reclaim_reserve_to(&env, &destination, sweep_id)?;

Ok(())
}
Expand Down Expand Up @@ -235,23 +237,92 @@ impl EphemeralAccountContract {
// Get total amount from all payments if any payments were received
let total_amount = if storage::has_payment_received(&env) {
let payments = storage::get_all_payments(&env);
payments
.iter()
.fold(0, |sum, (_, payment)| sum + payment.amount)
let mut total = 0i128;
for (_, payment) in payments.iter() {
total = total
.checked_add(payment.amount)
.ok_or(Error::InvalidAmount)?;
}
total
} else {
0
};

// Base reserve amount (1 XLM in stroops)
let reserve_amount = 1_000_000_000i128;
let sweep_id = env.ledger().sequence() as u64;
storage::set_last_sweep_id(&env, sweep_id);

// Reclaim reserve to recovery destination.
let reclaimed_reserve = Self::reclaim_reserve_to(&env, &recovery_address, sweep_id)?;

// Emit events for account expiration and reserve reclamation
events::emit_account_expired(&env, recovery_address.clone(), total_amount, reserve_amount);
events::emit_reserve_reclaimed(&env, recovery_address, reserve_amount);
// Emit expiration event with reserve amount reclaimed in this call.
events::emit_account_expired(&env, recovery_address, total_amount, reclaimed_reserve);

Ok(())
}

/// Reclaim remaining base reserve for a previously swept/expired account.
/// This is safe to call repeatedly: once fully reclaimed, subsequent calls transfer 0.
pub fn reclaim_reserve(env: Env) -> Result<i128, Error> {
if !storage::is_initialized(&env) {
return Err(Error::NotInitialized);
}

let status = storage::get_status(&env);
if status != AccountStatus::Swept && status != AccountStatus::Expired {
return Err(Error::InvalidStatus);
}

let destination = storage::get_swept_to(&env).ok_or(Error::InvalidStatus)?;
let sweep_id = storage::get_last_sweep_id(&env);

Self::reclaim_reserve_to(&env, &destination, sweep_id)
}

/// Remaining reserve amount (stroops) still eligible for reclaim.
pub fn get_reserve_remaining(env: Env) -> i128 {
if !storage::is_initialized(&env) {
return 0;
}

storage::get_base_reserve_remaining(&env)
}

/// Tracked reserve currently available for transfer (stroops).
pub fn get_reserve_available(env: Env) -> i128 {
if !storage::is_initialized(&env) {
return 0;
}

storage::get_available_reserve(&env)
}

/// Whether reserve has been fully reclaimed.
pub fn is_reserve_reclaimed(env: Env) -> bool {
if !storage::is_initialized(&env) {
return false;
}

storage::is_reserve_reclaimed(&env)
}

/// Last reserve reclaim event payload emitted by this contract.
pub fn get_last_reserve_event(env: Env) -> Option<ReserveReclaimed> {
if !storage::is_initialized(&env) {
return None;
}

storage::get_last_reserve_event(&env)
}

/// Number of reserve reclaim events emitted by this contract.
pub fn get_reserve_reclaim_event_count(env: Env) -> u32 {
if !storage::is_initialized(&env) {
return 0;
}

storage::get_reserve_event_count(&env)
}

/// Get account information
pub fn get_info(env: Env) -> Result<AccountInfo, Error> {
if !storage::is_initialized(&env) {
Expand Down Expand Up @@ -291,4 +362,72 @@ impl EphemeralAccountContract {
// Future: Verify signature against authorized signer
Ok(())
}

fn reclaim_reserve_to(env: &Env, destination: &Address, sweep_id: u64) -> Result<i128, Error> {
let reserve_remaining = storage::get_base_reserve_remaining(env);
let reserve_available = storage::get_available_reserve(env);

if reserve_remaining < 0 || reserve_available < 0 {
return Err(Error::InvalidAmount);
}

if reserve_remaining == 0 {
storage::set_reserve_reclaimed(env, true);
let event = ReserveReclaimed {
destination: destination.clone(),
amount: 0,
sweep_id,
fully_reclaimed: true,
remaining_reserve: 0,
};
Self::emit_and_store_reserve_event(env, event)?;
return Ok(0);
}

let reclaim_amount = if reserve_available < reserve_remaining {
reserve_available
} else {
reserve_remaining
};

let new_available = reserve_available
.checked_sub(reclaim_amount)
.ok_or(Error::InvalidAmount)?;
let new_remaining = reserve_remaining
.checked_sub(reclaim_amount)
.ok_or(Error::InvalidAmount)?;

storage::set_available_reserve(env, new_available);
storage::set_base_reserve_remaining(env, new_remaining);
storage::set_reserve_reclaimed(env, new_remaining == 0);

let event = ReserveReclaimed {
destination: destination.clone(),
amount: reclaim_amount,
sweep_id,
fully_reclaimed: new_remaining == 0,
remaining_reserve: new_remaining,
};
Self::emit_and_store_reserve_event(env, event)?;

Ok(reclaim_amount)
}

fn emit_and_store_reserve_event(env: &Env, event: ReserveReclaimed) -> Result<(), Error> {
events::emit_reserve_reclaimed(
env,
event.destination.clone(),
event.amount,
event.sweep_id,
event.fully_reclaimed,
event.remaining_reserve,
);

let event_count = storage::get_reserve_event_count(env);
let next_count = event_count.checked_add(1).ok_or(Error::InvalidAmount)?;
storage::set_last_reserve_event(env, &event);
storage::set_reserve_event_count(env, next_count);

Ok(())
}
}
91 changes: 91 additions & 0 deletions contracts/ephemeral_account/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::events::ReserveReclaimed;
use bridgelet_shared::{AccountStatus, Payment};
use soroban_sdk::{contracttype, Address, Env, Map};

Expand All @@ -10,6 +11,12 @@ pub enum DataKey {
Payments,
Status,
SweptTo,
BaseReserveRemaining,
AvailableReserve,
ReserveReclaimed,
LastSweepId,
ReserveEventCount,
LastReserveEvent,
}

// Initialization
Expand Down Expand Up @@ -113,3 +120,87 @@ pub fn set_swept_to(env: &Env, address: &Address) {
pub fn get_swept_to(env: &Env) -> Option<Address> {
env.storage().instance().get(&DataKey::SweptTo)
}

// Reserve lifecycle
pub fn init_reserve_tracking(env: &Env, base_reserve: i128) {
set_base_reserve_remaining(env, base_reserve);
set_available_reserve(env, base_reserve);
set_reserve_reclaimed(env, base_reserve == 0);
set_last_sweep_id(env, 0);
set_reserve_event_count(env, 0);
}

pub fn set_base_reserve_remaining(env: &Env, amount: i128) {
env.storage()
.instance()
.set(&DataKey::BaseReserveRemaining, &amount);
}

pub fn get_base_reserve_remaining(env: &Env) -> i128 {
env.storage()
.instance()
.get(&DataKey::BaseReserveRemaining)
.unwrap_or(0)
}

pub fn set_available_reserve(env: &Env, amount: i128) {
env.storage()
.instance()
.set(&DataKey::AvailableReserve, &amount);
}

pub fn get_available_reserve(env: &Env) -> i128 {
env.storage()
.instance()
.get(&DataKey::AvailableReserve)
.unwrap_or(0)
}

pub fn set_reserve_reclaimed(env: &Env, reclaimed: bool) {
env.storage()
.instance()
.set(&DataKey::ReserveReclaimed, &reclaimed);
}

pub fn is_reserve_reclaimed(env: &Env) -> bool {
env.storage()
.instance()
.get(&DataKey::ReserveReclaimed)
.unwrap_or(false)
}

pub fn set_last_sweep_id(env: &Env, sweep_id: u64) {
env.storage()
.instance()
.set(&DataKey::LastSweepId, &sweep_id);
}

pub fn get_last_sweep_id(env: &Env) -> u64 {
env.storage()
.instance()
.get(&DataKey::LastSweepId)
.unwrap_or(0)
}

pub fn set_reserve_event_count(env: &Env, count: u32) {
env.storage()
.instance()
.set(&DataKey::ReserveEventCount, &count);
}

pub fn get_reserve_event_count(env: &Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::ReserveEventCount)
.unwrap_or(0)
}

pub fn set_last_reserve_event(env: &Env, event: &ReserveReclaimed) {
env.storage()
.instance()
.set(&DataKey::LastReserveEvent, event);
}

pub fn get_last_reserve_event(env: &Env) -> Option<ReserveReclaimed> {
env.storage().instance().get(&DataKey::LastReserveEvent)
}
Loading
Loading