Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ members = [
"contracts/sweep_controller",
"contracts/shared",
"contracts/reserve_contract",
"contracts/native_transfer",
]
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ Handles fund transfers:
- Handles multi-asset transfers
- Reclaims base reserves

### 3. ReserveContract
Stores and exposes the Stellar base reserve configuration:
- Admin-controlled base reserve amount (stored in stroops)
- Distinguishes user funds from network overhead in ephemeral accounts
- TTL management to prevent contract data archival
- Event emission for reserve updates and auditability

## Project Structure

contracts/
Expand All @@ -47,13 +54,22 @@ contracts/
│ ├── src/
│ │ ├── lib.rs
│ │ ├── authorization.rs
│ │ └── transfers.rs
│ │ ├── transfers.rs
│ │ ├── storage.rs # State management
│ │ └── errors.rs # Error types
│ └── Cargo.toml
├── reserve_contract/ # ← NEW
│ ├── src/
│ │ ├── lib.rs # Main contract
│ │ ├── storage.rs # State management
│ │ ├── events.rs # Event definitions
│ │ └── errors.rs # Error types
│ └── Cargo.toml
└── shared/
└── types.rs # Shared types

├── src/
│ ├── lib.rs
│ └── types.rs
└── Cargo.toml
## Prerequisites
```bash
# Install Rust
Expand Down Expand Up @@ -110,13 +126,16 @@ pub trait EphemeralAccountInterface {
fn record_payment(env: Env, amount: i128, asset: Address) -> Result<(), Error>;

// Execute sweep to permanent wallet
fn sweep(env: Env, destination: Address) -> Result<(), Error>;
fn sweep(env: Env, destination: Address, auth_signature: BytesN<64>) -> Result<(), Error>;

// Check if account is expired
fn is_expired(env: Env) -> bool;
}
```
> **⚠️ MVP:** **authorization is not yet enforced on-chain.
> **⚠️ MVP:** On-chain authorization is not enforced at the `EphemeralAccount` contract
> level. Calling `EphemeralAccount::sweep()` directly bypasses all signature verification.
> Authorization is only enforced when sweeps are routed through `SweepController`.
> Do not call `EphemeralAccount::sweep()` directly in production.

See [Bridgelet Documentation](https://github.com/bridgelet-org/bridgelet) for full API reference.

Expand Down Expand Up @@ -148,4 +167,4 @@ See [Security Audit Report](./docs/security-audit.md) (coming soon)

## License

MIT
MIT
1 change: 1 addition & 0 deletions contracts/ephemeral_account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bridgelet-shared = { path = "../shared", version = "0.1.0" }

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
bridgelet-shared = { path = "../shared", features = ["testutils"] }

[profile.release]
opt-level = "z"
Expand Down
73 changes: 14 additions & 59 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ pub use events::{
};
pub use storage::DataKey;

const BASE_RESERVE_STROOPS: i128 = 1_000_000_000;
// Stellar Base Reserve Constants in Stroops (0.5 XLM = 5_000_000 Stroops)
const STELLAR_BASE_RESERVE_STROOPS: i128 = 5_000_000;

#[contract]
pub struct EphemeralAccountContract;

#[contractimpl]
impl EphemeralAccountContract {
/// Initialize the ephemeral account with restrictions
/// Initialize the ephemeral account with dynamically calculated base reserves
///
/// # Arguments
/// * `creator` - Address that created this account
Expand Down Expand Up @@ -52,13 +53,22 @@ impl EphemeralAccountContract {
return Err(Error::InvalidExpiry);
}

// --- Calculate Base Reserve Requirements ---
// Basic requirement: 1 Base Reserve for the account itself + 1 Base Reserve for tracking trustlines/storage footprint data
let total_entries = 2;
let calculated_reserve = STELLAR_BASE_RESERVE_STROOPS
.checked_mul(total_entries)
.ok_or(Error::InvalidAmount)?;

// Store initialization data
storage::set_initialized(&env, true);
storage::set_creator(&env, &creator);
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);

// Persist calculated reserve via storage layer tracking
storage::init_reserve_tracking(&env, calculated_reserve);

// Emit event
events::emit_account_created(&env, creator, expiry_ledger);
Expand All @@ -67,53 +77,36 @@ impl EphemeralAccountContract {
}

/// Record an inbound payment to this ephemeral account
/// Multiple payments with different assets are supported
///
/// # Arguments
/// * `amount` - Payment amount
/// * `asset` - Asset address
///
/// # Errors
/// 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);
}

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

// Check for duplicate asset
if storage::get_payment(&env, &asset).is_some() {
return Err(Error::DuplicateAsset);
}

// 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 {
Expand All @@ -124,61 +117,39 @@ impl EphemeralAccountContract {
}

/// Execute sweep to destination wallet
/// Transfers all funds from all assets to the specified destination atomically
///
/// # Arguments
/// * `destination` - Recipient wallet address
/// * `auth_signature` - Authorization signature from off-chain system
///
/// # Errors
/// Returns Error::Unauthorized if authorization fails
/// Returns Error::AlreadySwept if sweep already executed
pub fn sweep(env: Env, destination: Address, auth_signature: BytesN<64>) -> Result<(), Error> {
// Check initialized
if !storage::is_initialized(&env) {
return Err(Error::NotInitialized);
}

// Check not already swept
if storage::get_status(&env) == AccountStatus::Swept {
return Err(Error::AlreadySwept);
}

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

// Check not expired
if Self::is_expired(env.clone()) {
return Err(Error::AccountExpired);
}

// Verify authorization signature
// Note: In production, implement proper signature verification
// For MVP, we trust the SDK to only call with valid signatures
Self::verify_sweep_authorization(&env, &destination, &auth_signature)?;

// 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 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 sweep event once transfer authorization/state update succeeds.
events::emit_sweep_executed_multi(&env, destination.clone(), &payments_vec);

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

Ok(())
Expand Down Expand Up @@ -206,35 +177,25 @@ impl EphemeralAccountContract {
}

/// Expire the account and return funds to recovery address
/// Can only be called after expiry ledger is reached
///
/// # Errors
/// Returns Error::NotExpired if called before expiry ledger
pub fn expire(env: Env) -> Result<(), Error> {
// Check initialized
if !storage::is_initialized(&env) {
return Err(Error::NotInitialized);
}

// Check not already swept or expired
let status = storage::get_status(&env);
if status == AccountStatus::Swept || status == AccountStatus::Expired {
return Err(Error::InvalidStatus);
}

// Check if expired
if !Self::is_expired(env.clone()) {
return Err(Error::NotExpired);
}

// Get recovery address
let recovery_address = storage::get_recovery_address(&env);

// Update status
storage::set_status(&env, AccountStatus::Expired);
storage::set_swept_to(&env, &recovery_address);

// 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);
let mut total = 0i128;
Expand All @@ -251,17 +212,14 @@ impl EphemeralAccountContract {
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 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);
Expand Down Expand Up @@ -357,9 +315,6 @@ impl EphemeralAccountContract {
_destination: &Address,
_signature: &BytesN<64>,
) -> Result<(), Error> {
// TODO: Implement proper signature verification
// For MVP, we rely on off-chain SDK to only call with valid auth
// Future: Verify signature against authorized signer
Ok(())
}

Expand Down Expand Up @@ -430,4 +385,4 @@ impl EphemeralAccountContract {

Ok(())
}
}
}
18 changes: 18 additions & 0 deletions contracts/native_transfer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "native-transfer"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["rlib", "cdylib"]

[dependencies]
soroban-sdk = "22.0.0"
bridgelet-shared = { path = "../shared" }

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
bridgelet-shared = { path = "../shared", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]
8 changes: 8 additions & 0 deletions contracts/native_transfer/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Error {
InvalidAmount = 1,
TransferFailed = 2,
}
14 changes: 14 additions & 0 deletions contracts/native_transfer/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use soroban_sdk::{contracttype, symbol_short, Address, Env};

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

pub fn emit_native_transfer_executed(env: &Env, from: Address, to: Address, amount: i128) {
let event = NativeTransferExecuted { from, to, amount };
env.events().publish((symbol_short!("native_tx"),), event);
}
17 changes: 17 additions & 0 deletions contracts/native_transfer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#![no_std]

mod errors;
mod events;

use soroban_sdk::{contract, contractimpl, Env};

pub use errors::Error;
pub use events::NativeTransferExecuted;

#[contract]
pub struct NativeTransferContract;

#[contractimpl]
impl NativeTransferContract {
// Implementation coming in a future issue
}
1 change: 1 addition & 0 deletions contracts/reserve_contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ soroban-sdk = "22.0.0"

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
bridgelet-shared = { path = "../shared", features = ["testutils"] }

[profile.release]
opt-level = "z"
Expand Down
3 changes: 3 additions & 0 deletions contracts/shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ edition = "2021"
[dependencies]
soroban-sdk = "22.0.0"

[features]
testutils = ["soroban-sdk/testutils"]

[lib]
crate-type = ["rlib"]
2 changes: 1 addition & 1 deletion contracts/shared/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct Payment {
}
// The current status of an ephemeral account.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Copy)]
#[repr(u32)]
pub enum AccountStatus {
Active = 0,
Expand Down
Loading
Loading