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
2 changes: 2 additions & 0 deletions contracts/ephemeral_account/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ pub enum Error {
NoPaymentReceived = 10,
AccountExpired = 11,
InvalidStatus = 12,
DuplicateAsset = 13,
TooManyPayments = 14,
}
30 changes: 29 additions & 1 deletion contracts/ephemeral_account/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use soroban_sdk::{contracttype, symbol_short, Address, Env};
use crate::storage::Payment;
use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
Expand All @@ -22,6 +23,20 @@ pub struct SweepExecuted {
pub asset: Address,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SweepExecutedMulti {
pub destination: Address,
pub payments: Vec<Payment>,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiPaymentReceived {
pub asset: Address,
pub amount: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccountExpired {
Expand Down Expand Up @@ -51,6 +66,19 @@ pub fn emit_sweep_executed(env: &Env, destination: Address, amount: i128, asset:
env.events().publish((symbol_short!("swept"),), event);
}

pub fn emit_sweep_executed_multi(env: &Env, destination: Address, payments: &Vec<Payment>) {
let event = SweepExecutedMulti {
destination,
payments: payments.clone(),
};
env.events().publish((symbol_short!("swept_mul"),), event);
}

pub fn emit_multi_payment_received(env: &Env, asset: Address, amount: i128) {
let event = MultiPaymentReceived { asset, amount };
env.events().publish((symbol_short!("multi_pay"),), event);
}

pub fn emit_account_expired(env: &Env, recovery_address: Address, amount_returned: i128) {
let event = AccountExpired {
recovery_address,
Expand Down
108 changes: 73 additions & 35 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
mod errors;
mod events;
mod storage;
#[cfg(test)]
mod test;

use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env};
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec};

pub use errors::Error;
pub use events::{AccountCreated, AccountExpired, PaymentReceived, SweepExecuted};
pub use storage::{AccountStatus, DataKey};
pub use events::{
AccountCreated, AccountExpired, MultiPaymentReceived, PaymentReceived, SweepExecuted,
};
pub use storage::{AccountStatus, DataKey, Payment};

#[contract]
pub struct EphemeralAccountContract;
Expand Down Expand Up @@ -58,44 +62,64 @@ impl EphemeralAccountContract {
}

/// Record an inbound payment to this ephemeral account
/// Only the first payment is accepted
/// Multiple payments with different assets are supported
///
/// # Arguments
/// * `amount` - Payment amount
/// * `asset` - Asset address
///
/// # Errors
/// Returns Error::PaymentAlreadyReceived if payment already recorded
/// Returns Error::InvalidAmount if amount is not positive
/// Returns Error::DuplicateAsset if asset already has a payment
pub fn record_payment(env: Env, amount: i128, asset: Address) -> Result<(), Error> {
// Check initialized
if !storage::is_initialized(&env) {
return Err(Error::NotInitialized);
}

// Check if payment already received
if storage::has_payment_received(&env) {
return Err(Error::PaymentAlreadyReceived);
}

// Validate amount
if amount <= 0 {
return Err(Error::InvalidAmount);
}

// Record payment
storage::set_payment_received(&env, true);
storage::set_payment_amount(&env, amount);
storage::set_payment_asset(&env, &asset);
storage::set_status(&env, AccountStatus::PaymentReceived);
// Check for duplicate asset
if storage::get_payment(&env, &asset).is_some() {
return Err(Error::DuplicateAsset);
}

// Emit event
events::emit_payment_received(&env, amount, asset);
// Check payment limit to prevent gas issues (max 10 assets)
let payment_count = storage::get_total_payments(&env);
if payment_count >= 10 {
return Err(Error::TooManyPayments);
}

// Create payment with current timestamp
let payment = Payment {
asset: asset.clone(),
amount,
timestamp: env.ledger().timestamp(),
};

// Add payment
storage::add_payment(&env, payment);

// Update status only on first payment
if payment_count == 0 {
storage::set_status(&env, AccountStatus::PaymentReceived);
}

// Emit appropriate event
if payment_count == 0 {
events::emit_payment_received(&env, amount, asset);
} else {
events::emit_multi_payment_received(&env, asset, amount);
}

Ok(())
}

/// Execute sweep to destination wallet
/// Transfers all funds to the specified destination
/// Transfers all funds from all assets to the specified destination atomically
///
/// # Arguments
/// * `destination` - Recipient wallet address
Expand Down Expand Up @@ -130,20 +154,24 @@ impl EphemeralAccountContract {
// For MVP, we trust the SDK to only call with valid signatures
Self::verify_sweep_authorization(&env, &destination, &auth_signature)?;

// Get payment details
let amount = storage::get_payment_amount(&env);
let asset = storage::get_payment_asset(&env);
// Get all payments
let payments = storage::get_all_payments(&env);
let mut payments_vec = Vec::new(&env);
for payment in payments.values() {
payments_vec.push_back(payment);
}

// Update status before transfer to prevent reentrancy
storage::set_status(&env, AccountStatus::Swept);
storage::set_swept_to(&env, &destination);

// Note: Actual token transfer happens in the SDK via Stellar SDK
// 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 the transfer
// The SDK will call this function, get approval, then execute all transfers atomically
// All transfers must succeed or the entire operation fails

// Emit event
events::emit_sweep_executed(&env, destination, amount, asset);
// Emit event with all assets
events::emit_sweep_executed_multi(&env, destination, &payments_vec);

Ok(())
}
Expand Down Expand Up @@ -198,15 +226,18 @@ impl EphemeralAccountContract {
storage::set_status(&env, AccountStatus::Expired);
storage::set_swept_to(&env, &recovery_address);

// Get payment details if payment was received
let amount = if storage::has_payment_received(&env) {
storage::get_payment_amount(&env)
// 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)
} else {
0
};

// Emit event
events::emit_account_expired(&env, recovery_address, amount);
events::emit_account_expired(&env, recovery_address, total_amount);

Ok(())
}
Expand All @@ -217,16 +248,22 @@ impl EphemeralAccountContract {
return Err(Error::NotInitialized);
}

let payments = storage::get_all_payments(&env);
let payment_count = payments.len();

Ok(AccountInfo {
creator: storage::get_creator(&env),
status: storage::get_status(&env),
expiry_ledger: storage::get_expiry_ledger(&env),
recovery_address: storage::get_recovery_address(&env),
payment_received: storage::has_payment_received(&env),
payment_amount: if storage::has_payment_received(&env) {
Some(storage::get_payment_amount(&env))
} else {
None
payment_received: payment_count > 0,
payment_count,
payments: {
let mut payments_vec = Vec::new(&env);
for payment in payments.values() {
payments_vec.push_back(payment);
}
payments_vec
},
swept_to: storage::get_swept_to(&env),
})
Expand Down Expand Up @@ -255,6 +292,7 @@ pub struct AccountInfo {
pub expiry_ledger: u32,
pub recovery_address: Address,
pub payment_received: bool,
pub payment_amount: Option<i128>,
pub payment_count: u32,
pub payments: Vec<Payment>,
pub swept_to: Option<Address>,
}
58 changes: 32 additions & 26 deletions contracts/ephemeral_account/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use soroban_sdk::{contracttype, Address, Env};
use soroban_sdk::{contracttype, Address, Env, Map};

#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct Payment {
pub asset: Address,
pub amount: i128,
pub timestamp: u64,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[contracttype]
pub enum AccountStatus {
Active = 0,
Expand All @@ -16,9 +24,7 @@ pub enum DataKey {
Creator,
ExpiryLedger,
RecoveryAddress,
PaymentReceived,
PaymentAmount,
PaymentAsset,
Payments,
Status,
SweptTo,
}
Expand Down Expand Up @@ -69,39 +75,39 @@ pub fn get_recovery_address(env: &Env) -> Address {
.unwrap()
}

// Payment
pub fn has_payment_received(env: &Env) -> bool {
env.storage().instance().has(&DataKey::PaymentReceived)
// Payments
pub fn has_payments(env: &Env) -> bool {
env.storage().instance().has(&DataKey::Payments)
}

pub fn set_payment_received(env: &Env, value: bool) {
pub fn get_all_payments(env: &Env) -> Map<Address, Payment> {
env.storage()
.instance()
.set(&DataKey::PaymentReceived, &value);
.get(&DataKey::Payments)
.unwrap_or_else(|| Map::new(env))
}

pub fn set_payment_amount(env: &Env, amount: i128) {
env.storage()
.instance()
.set(&DataKey::PaymentAmount, &amount);
pub fn set_all_payments(env: &Env, payments: &Map<Address, Payment>) {
env.storage().instance().set(&DataKey::Payments, payments);
}

pub fn get_payment_amount(env: &Env) -> i128 {
env.storage()
.instance()
.get(&DataKey::PaymentAmount)
.unwrap_or(0)
pub fn add_payment(env: &Env, payment: Payment) {
let mut payments = get_all_payments(env);
payments.set(payment.asset.clone(), payment);
set_all_payments(env, &payments);
}

pub fn set_payment_asset(env: &Env, asset: &Address) {
env.storage().instance().set(&DataKey::PaymentAsset, asset);
pub fn get_payment(env: &Env, asset: &Address) -> Option<Payment> {
let payments = get_all_payments(env);
payments.get(asset.clone())
}

pub fn get_payment_asset(env: &Env) -> Address {
env.storage()
.instance()
.get(&DataKey::PaymentAsset)
.unwrap()
pub fn get_total_payments(env: &Env) -> u32 {
get_all_payments(env).len()
}

pub fn has_payment_received(env: &Env) -> bool {
has_payments(env)
}

// Status
Expand Down
Loading
Loading