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