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/README.md b/README.md index 0be5bd8..b0206ff 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 @@ -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. @@ -148,4 +167,4 @@ See [Security Audit Report](./docs/security-audit.md) (coming soon) ## License -MIT \ No newline at end of file +MIT 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/ephemeral_account/src/lib.rs b/contracts/ephemeral_account/src/lib.rs index ad227e8..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,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(()) } @@ -430,4 +385,4 @@ impl EphemeralAccountContract { Ok(()) } -} +} \ No newline at end of file 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 c85e819..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}; @@ -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 @@ -116,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);