From b79848a7a2aa00b9e08884d7eab4fe9033f298de Mon Sep 17 00:00:00 2001 From: abdoolyaro <148596582+abdoolyaro@users.noreply.github.com> Date: Wed, 27 May 2026 11:14:07 +0100 Subject: [PATCH 1/7] Fixed auth_signature parameter to sweep function --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0be5bd8..040aca4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ 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; @@ -148,4 +148,4 @@ See [Security Audit Report](./docs/security-audit.md) (coming soon) ## License -MIT \ No newline at end of file +MIT From fb22117d2b1fae52b97dd1755665f2e0340898f4 Mon Sep 17 00:00:00 2001 From: abdoolyaro <148596582+abdoolyaro@users.noreply.github.com> Date: Wed, 27 May 2026 11:19:51 +0100 Subject: [PATCH 2/7] Clarify signature verification and authorization checks Added warning about the lack of on-chain signature verification and clarified authorization checks. --- contracts/ephemeral_account/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/ephemeral_account/src/lib.rs b/contracts/ephemeral_account/src/lib.rs index ad227e8..6b72853 100644 --- a/contracts/ephemeral_account/src/lib.rs +++ b/contracts/ephemeral_account/src/lib.rs @@ -357,9 +357,12 @@ 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 + // ⚠️ MVP STUB: Signature verification is NOT enforced on-chain in this contract. + // Calling EphemeralAccount::sweep() directly bypasses all authorization checks. + // Authorization is only enforced when going through SweepController, which + // performs Ed25519 signature verification via authorization.rs. + // TODO: Implement on-chain signature verification against an authorized signer + // before production use. Ok(()) } From fc8033a206a6564b2234f4f372ccc86c9e500f95 Mon Sep 17 00:00:00 2001 From: abdoolyaro <148596582+abdoolyaro@users.noreply.github.com> Date: Wed, 27 May 2026 11:21:37 +0100 Subject: [PATCH 3/7] Update MVP warning for EphemeralAccount contract Clarified MVP warning about on-chain authorization enforcement. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 040aca4..62362a5 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,10 @@ pub trait EphemeralAccountInterface { 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. From 3b2f9cf881d91a69f2149fdbf4d52012e347fcba Mon Sep 17 00:00:00 2001 From: abdoolyaro <148596582+abdoolyaro@users.noreply.github.com> Date: Wed, 27 May 2026 11:34:11 +0100 Subject: [PATCH 4/7] Modify initialize to accept creator address Updated the initialize function to accept and verify the creator address. --- contracts/sweep_controller/src/lib.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/sweep_controller/src/lib.rs b/contracts/sweep_controller/src/lib.rs index c85e819..8c556a0 100644 --- a/contracts/sweep_controller/src/lib.rs +++ b/contracts/sweep_controller/src/lib.rs @@ -30,17 +30,16 @@ impl SweepController { env: Env, authorized_signer: BytesN<32>, authorized_destination: Option
, + creator: Address, ) -> Result<(), Error> { // Check if already initialized if storage::get_authorized_signer(&env).is_some() { return Err(Error::AuthorizationFailed); } - // Store the creator address - // In Soroban SDK 22.0.0, we need to pass creator as a parameter - // For now, we'll use the contract address as a placeholder - // TODO: Update to accept creator as parameter if needed - let creator = env.current_contract_address(); + + // Verify and store the creator address + creator.require_auth(); storage::set_creator(&env, &creator); // Store the authorized signer public key From e72583b8dc30d66280ae3e2a8a9d83d19ab5c06b Mon Sep 17 00:00:00 2001 From: Ummi-001 Date: Sat, 30 May 2026 03:28:47 +0100 Subject: [PATCH 5/7] docs: add ReserveContract to README.md (#38) --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0be5bd8..ac2193d 100644 --- a/README.md +++ b/README.md @@ -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/ @@ -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 From 09f52d0f57f9e8b84333ab5b8386f8943115b998 Mon Sep 17 00:00:00 2001 From: bigben-7 Date: Sat, 30 May 2026 22:45:34 +0100 Subject: [PATCH 6/7] fix: enable transfers, testutils feature, AccountStatus ordering, scaffold native_transfer - Issue #42: uncomment `mod transfers` in sweep_controller and replace the dead-commented transfer block with a proper loop over `info.payments` using `transfers::TransferContext` - Issue #43: add `[features] testutils = ["soroban-sdk/testutils"]` to bridgelet-shared, and add `bridgelet-shared = { features = ["testutils"] }` to dev-dependencies in ephemeral_account, sweep_controller, and reserve_contract - Issue #44: add `PartialOrd, Ord` to AccountStatus derive so contracts can do ordered comparisons against the `#[repr(u32)]` state progression - Issue #48: scaffold contracts/native_transfer with Cargo.toml, src/lib.rs, src/errors.rs, and src/events.rs matching the existing contract pattern; registered in workspace Cargo.toml Closes #42Closes #43Closes #44Closes #48 --- Cargo.toml | 1 + contracts/ephemeral_account/Cargo.toml | 1 + contracts/native_transfer/Cargo.toml | 18 ++++++++++++++++++ contracts/native_transfer/src/errors.rs | 8 ++++++++ contracts/native_transfer/src/events.rs | 14 ++++++++++++++ contracts/native_transfer/src/lib.rs | 17 +++++++++++++++++ contracts/reserve_contract/Cargo.toml | 1 + contracts/shared/Cargo.toml | 3 +++ contracts/shared/src/types.rs | 2 +- contracts/sweep_controller/Cargo.toml | 1 + contracts/sweep_controller/src/lib.rs | 21 +++++++++++---------- 11 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 contracts/native_transfer/Cargo.toml create mode 100644 contracts/native_transfer/src/errors.rs create mode 100644 contracts/native_transfer/src/events.rs create mode 100644 contracts/native_transfer/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 9859e32..5b7cdaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ members = [ "contracts/sweep_controller", "contracts/shared", "contracts/reserve_contract", + "contracts/native_transfer", ] diff --git a/contracts/ephemeral_account/Cargo.toml b/contracts/ephemeral_account/Cargo.toml index 062d946..9077776 100644 --- a/contracts/ephemeral_account/Cargo.toml +++ b/contracts/ephemeral_account/Cargo.toml @@ -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" diff --git a/contracts/native_transfer/Cargo.toml b/contracts/native_transfer/Cargo.toml new file mode 100644 index 0000000..7ca7b6f --- /dev/null +++ b/contracts/native_transfer/Cargo.toml @@ -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"] diff --git a/contracts/native_transfer/src/errors.rs b/contracts/native_transfer/src/errors.rs new file mode 100644 index 0000000..a3bf0e6 --- /dev/null +++ b/contracts/native_transfer/src/errors.rs @@ -0,0 +1,8 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + InvalidAmount = 1, + TransferFailed = 2, +} diff --git a/contracts/native_transfer/src/events.rs b/contracts/native_transfer/src/events.rs new file mode 100644 index 0000000..96e1694 --- /dev/null +++ b/contracts/native_transfer/src/events.rs @@ -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); +} diff --git a/contracts/native_transfer/src/lib.rs b/contracts/native_transfer/src/lib.rs new file mode 100644 index 0000000..7303357 --- /dev/null +++ b/contracts/native_transfer/src/lib.rs @@ -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 +} diff --git a/contracts/reserve_contract/Cargo.toml b/contracts/reserve_contract/Cargo.toml index 555be97..66ef143 100644 --- a/contracts/reserve_contract/Cargo.toml +++ b/contracts/reserve_contract/Cargo.toml @@ -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" diff --git a/contracts/shared/Cargo.toml b/contracts/shared/Cargo.toml index e219632..0727337 100644 --- a/contracts/shared/Cargo.toml +++ b/contracts/shared/Cargo.toml @@ -6,5 +6,8 @@ edition = "2021" [dependencies] soroban-sdk = "22.0.0" +[features] +testutils = ["soroban-sdk/testutils"] + [lib] crate-type = ["rlib"] diff --git a/contracts/shared/src/types.rs b/contracts/shared/src/types.rs index 6e2387d..7abc7a1 100644 --- a/contracts/shared/src/types.rs +++ b/contracts/shared/src/types.rs @@ -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, diff --git a/contracts/sweep_controller/Cargo.toml b/contracts/sweep_controller/Cargo.toml index 4319bab..f5c4b97 100644 --- a/contracts/sweep_controller/Cargo.toml +++ b/contracts/sweep_controller/Cargo.toml @@ -15,6 +15,7 @@ soroban-token-sdk = "22.0.0" [dev-dependencies] soroban-sdk = { version = "22.0.0", features = ["testutils"] } +bridgelet-shared = { path = "../shared", features = ["testutils"] } [profile.release] diff --git a/contracts/sweep_controller/src/lib.rs b/contracts/sweep_controller/src/lib.rs index 8c556a0..068dcb6 100644 --- a/contracts/sweep_controller/src/lib.rs +++ b/contracts/sweep_controller/src/lib.rs @@ -3,7 +3,7 @@ mod authorization; mod errors; mod storage; -// mod transfers; +mod transfers; use ephemeral_account::EphemeralAccountContractClient as EphemeralAccountClient; use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env}; @@ -115,15 +115,16 @@ impl SweepController { return Err(Error::AccountNotReady); } - // Execute the actual token transfer - // Note: In production, the ephemeral account would need to authorize this transfer - // let transfer_ctx = TransferContext::new( - // info.payment_asset, - // ephemeral_account.clone(), - // destination.clone(), - // amount, - // ); - // transfer_ctx.execute(&env)?; + // Execute the actual token transfer for each payment asset + for payment in info.payments.iter() { + let transfer_ctx = transfers::TransferContext::new( + payment.asset.clone(), + ephemeral_account.clone(), + destination.clone(), + payment.amount, + ); + transfer_ctx.execute(&env)?; + } // Emit sweep executed event emit_sweep_completed(&env, ephemeral_account, destination, amount); From 9b77cfa090f00918cd0d421a80dc823b15a649fb Mon Sep 17 00:00:00 2001 From: Ummi-001 Date: Sun, 31 May 2026 22:13:13 +0100 Subject: [PATCH 7/7] feat: dynamically calculate and store base reserve during initialization (#27) --- contracts/ephemeral_account/src/lib.rs | 76 +++++--------------------- 1 file changed, 14 insertions(+), 62 deletions(-) diff --git a/contracts/ephemeral_account/src/lib.rs b/contracts/ephemeral_account/src/lib.rs index 6b72853..d362142 100644 --- a/contracts/ephemeral_account/src/lib.rs +++ b/contracts/ephemeral_account/src/lib.rs @@ -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 @@ -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); @@ -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 { @@ -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(()) @@ -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; @@ -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 { if !storage::is_initialized(&env) { return Err(Error::NotInitialized); @@ -357,12 +315,6 @@ impl EphemeralAccountContract { _destination: &Address, _signature: &BytesN<64>, ) -> Result<(), Error> { - // ⚠️ MVP STUB: Signature verification is NOT enforced on-chain in this contract. - // Calling EphemeralAccount::sweep() directly bypasses all authorization checks. - // Authorization is only enforced when going through SweepController, which - // performs Ed25519 signature verification via authorization.rs. - // TODO: Implement on-chain signature verification against an authorized signer - // before production use. Ok(()) } @@ -433,4 +385,4 @@ impl EphemeralAccountContract { Ok(()) } -} +} \ No newline at end of file