diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3f6deba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "contracts/ephemeral_account", + "contracts/sweep_controller", + "contracts/shared", +] diff --git a/contracts/ephemeral_account/Cargo.toml b/contracts/ephemeral_account/Cargo.toml index dc80589..062d946 100644 --- a/contracts/ephemeral_account/Cargo.toml +++ b/contracts/ephemeral_account/Cargo.toml @@ -4,10 +4,12 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "22.0.0" +bridgelet-shared = { path = "../shared", version = "0.1.0" } + [dev-dependencies] soroban-sdk = { version = "22.0.0", features = ["testutils"] } diff --git a/contracts/ephemeral_account/src/events.rs b/contracts/ephemeral_account/src/events.rs index edc9718..49548c4 100644 --- a/contracts/ephemeral_account/src/events.rs +++ b/contracts/ephemeral_account/src/events.rs @@ -1,4 +1,4 @@ -use crate::storage::Payment; +use bridgelet_shared::Payment; use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; #[contracttype] diff --git a/contracts/ephemeral_account/src/lib.rs b/contracts/ephemeral_account/src/lib.rs index 54b411c..b6e1a07 100644 --- a/contracts/ephemeral_account/src/lib.rs +++ b/contracts/ephemeral_account/src/lib.rs @@ -6,13 +6,14 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec}; +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Vec}; +pub use bridgelet_shared::{AccountInfo, AccountStatus, Payment}; pub use errors::Error; pub use events::{ AccountCreated, AccountExpired, MultiPaymentReceived, PaymentReceived, SweepExecutedMulti, }; -pub use storage::{AccountStatus, DataKey, Payment}; +pub use storage::DataKey; #[contract] pub struct EphemeralAccountContract; @@ -282,17 +283,3 @@ impl EphemeralAccountContract { Ok(()) } } - -/// Account information structure -#[derive(Clone)] -#[contracttype] -pub struct AccountInfo { - pub creator: Address, - pub status: AccountStatus, - pub expiry_ledger: u32, - pub recovery_address: Address, - pub payment_received: bool, - pub payment_count: u32, - pub payments: Vec, - pub swept_to: Option
, -} diff --git a/contracts/ephemeral_account/src/storage.rs b/contracts/ephemeral_account/src/storage.rs index 73fd04b..30a3921 100644 --- a/contracts/ephemeral_account/src/storage.rs +++ b/contracts/ephemeral_account/src/storage.rs @@ -1,22 +1,6 @@ +use bridgelet_shared::{AccountStatus, Payment}; use soroban_sdk::{contracttype, Address, Env, Map}; -#[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, - PaymentReceived = 1, - Swept = 2, - Expired = 3, -} - #[contracttype] pub enum DataKey { Initialized, diff --git a/contracts/shared/Cargo.toml b/contracts/shared/Cargo.toml new file mode 100644 index 0000000..e219632 --- /dev/null +++ b/contracts/shared/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bridgelet-shared" +version = "0.1.0" +edition = "2021" + +[dependencies] +soroban-sdk = "22.0.0" + +[lib] +crate-type = ["rlib"] diff --git a/contracts/shared/src/lib.rs b/contracts/shared/src/lib.rs new file mode 100644 index 0000000..8162091 --- /dev/null +++ b/contracts/shared/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] + +mod types; + +pub use types::{AccountInfo, AccountStatus, Payment}; diff --git a/contracts/shared/src/types.rs b/contracts/shared/src/types.rs new file mode 100644 index 0000000..6e2387d --- /dev/null +++ b/contracts/shared/src/types.rs @@ -0,0 +1,34 @@ +use soroban_sdk::{contracttype, Address, Vec}; + +// Represents a payment received by the ephemeral account. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Payment { + pub asset: Address, + pub amount: i128, + pub timestamp: u64, +} +// The current status of an ephemeral account. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +#[repr(u32)] +pub enum AccountStatus { + Active = 0, + PaymentReceived = 1, + Swept = 2, + Expired = 3, +} + +/// Account information structure +#[derive(Clone)] +#[contracttype] +pub struct AccountInfo { + pub creator: Address, + pub status: AccountStatus, + pub expiry_ledger: u32, + pub recovery_address: Address, + pub payment_received: bool, + pub payment_count: u32, + pub payments: Vec, + pub swept_to: Option
, +} diff --git a/contracts/sweep_controller/Cargo.toml b/contracts/sweep_controller/Cargo.toml index 9a488a5..242828b 100644 --- a/contracts/sweep_controller/Cargo.toml +++ b/contracts/sweep_controller/Cargo.toml @@ -4,14 +4,18 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "22.0.0" +bridgelet-shared = { path = "../shared", version = "0.1.0" } + soroban-token-sdk = "22.0.0" [dev-dependencies] soroban-sdk = { version = "22.0.0", features = ["testutils"] } +ephemeral_account = { path = "../ephemeral_account" } + [profile.release] opt-level = "z" diff --git a/contracts/sweep_controller/src/authorization.rs b/contracts/sweep_controller/src/authorization.rs index 03a6a7d..6e254f7 100644 --- a/contracts/sweep_controller/src/authorization.rs +++ b/contracts/sweep_controller/src/authorization.rs @@ -1,6 +1,6 @@ use crate::errors::Error; use crate::storage; -use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env}; +use soroban_sdk::{xdr::ToXdr, Address, BytesN, Env}; /// Construct the message to be signed for sweep authorization /// @@ -13,56 +13,35 @@ use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env}; /// /// # Returns /// BytesN<32> containing the hash of the message components -fn construct_sweep_message( - env: &Env, - destination: &Address, - contract_id: &Address, -) -> BytesN<32> { +fn construct_sweep_message(env: &Env, destination: &Address, contract_id: &Address) -> BytesN<32> { // Get current nonce let nonce = storage::get_sweep_nonce(env); // Construct the message by concatenating: // - destination (serialized as bytes) - // - nonce (as u64, 8 bytes) + // - nonce (as u64, 8 bytes) // - contract_id (serialized as bytes) - - // Get XDR bytes for addresses + let mut message = soroban_sdk::Bytes::new(env); + + // Add destination address bytes let dest_bytes = destination.to_xdr(env); + message.append(&dest_bytes); + + // Add nonce bytes (big-endian u64) + message.push_back(((nonce >> 56) & 0xFF) as u8); + message.push_back(((nonce >> 48) & 0xFF) as u8); + message.push_back(((nonce >> 40) & 0xFF) as u8); + message.push_back(((nonce >> 32) & 0xFF) as u8); + message.push_back(((nonce >> 24) & 0xFF) as u8); + message.push_back(((nonce >> 16) & 0xFF) as u8); + message.push_back(((nonce >> 8) & 0xFF) as u8); + message.push_back((nonce & 0xFF) as u8); + + // Add contract id bytes let contract_bytes = contract_id.to_xdr(env); - - // Build nonce bytes (big-endian u64) as BytesN<8> then convert to Bytes - let nonce_array = [ - ((nonce >> 56) & 0xFF) as u8, - ((nonce >> 48) & 0xFF) as u8, - ((nonce >> 40) & 0xFF) as u8, - ((nonce >> 32) & 0xFF) as u8, - ((nonce >> 24) & 0xFF) as u8, - ((nonce >> 16) & 0xFF) as u8, - ((nonce >> 8) & 0xFF) as u8, - (nonce & 0xFF) as u8, - ]; - let nonce_bytes_n = BytesN::from_array(env, &nonce_array); - let nonce_bytes: Bytes = nonce_bytes_n.into(); - - // Build message by concatenating bytes - let mut message = Bytes::new(env); - - // Copy bytes into message one by one - let mut idx = 0u32; - for i in 0..dest_bytes.len() { - message.set(idx, dest_bytes.get(i).unwrap()); - idx += 1; - } - for i in 0..nonce_bytes.len() { - message.set(idx, nonce_bytes.get(i).unwrap()); - idx += 1; - } - for i in 0..contract_bytes.len() { - message.set(idx, contract_bytes.get(i).unwrap()); - idx += 1; - } + message.append(&contract_bytes); - // Hash the message using SHA256 and convert to BytesN<32> + // Hash the message using SHA256 env.crypto().sha256(&message).into() } @@ -86,8 +65,8 @@ pub fn verify_sweep_auth( signature: &BytesN<64>, ) -> Result<(), Error> { // Get the authorized signer public key from storage - let authorized_signer = storage::get_authorized_signer(env) - .ok_or(Error::AuthorizedSignerNotSet)?; + let authorized_signer = + storage::get_authorized_signer(env).ok_or(Error::AuthorizedSignerNotSet)?; // Get the sweep controller contract address let contract_id = env.current_contract_address(); @@ -96,18 +75,8 @@ pub fn verify_sweep_auth( let message = construct_sweep_message(env, destination, &contract_id); // Verify the Ed25519 signature - // ed25519_verify expects: - // - &BytesN<32> for public key - // - &Bytes for message (the hash) - // - &BytesN<64> for signature - // Convert message from BytesN<32> to Bytes - let message_bytes: Bytes = message.into(); - - // ed25519_verify returns () and panics on failure - // In Soroban, panics are caught by the execution environment - // We'll call it directly - if it panics, the contract execution will fail - env.crypto().ed25519_verify(&authorized_signer, &message_bytes, signature); - + env.crypto() + .ed25519_verify(&authorized_signer, &message.into(), signature); Ok(()) } diff --git a/contracts/sweep_controller/src/lib.rs b/contracts/sweep_controller/src/lib.rs index fd4570c..b5bb7d8 100644 --- a/contracts/sweep_controller/src/lib.rs +++ b/contracts/sweep_controller/src/lib.rs @@ -3,11 +3,13 @@ mod authorization; mod errors; mod storage; -mod transfers; +// mod transfers; +use crate::ephemeral_account::Client as EphemeralAccountClient; use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env}; use authorization::AuthContext; +use bridgelet_shared::AccountStatus; pub use errors::Error; #[contract] @@ -76,8 +78,8 @@ impl SweepController { ) -> Result<(), Error> { // Validate destination if authorized destination is set (locked mode) if storage::has_authorized_destination(&env) { - let authorized_dest = storage::get_authorized_destination(&env) - .ok_or(Error::UnauthorizedDestination)?; + let authorized_dest = + storage::get_authorized_destination(&env).ok_or(Error::UnauthorizedDestination)?; if destination != authorized_dest { return Err(Error::UnauthorizedDestination); } @@ -96,7 +98,7 @@ impl SweepController { // Call ephemeral account contract to validate and authorize sweep // This triggers the account's sweep() method which updates state - let account_client = ephemeral_account::Client::new(&env, &ephemeral_account); + let account_client = EphemeralAccountClient::new(&env, &ephemeral_account); // The account contract validates state and authorizes the sweep account_client.sweep(&destination, &auth_signature); @@ -109,15 +111,10 @@ impl SweepController { return Err(Error::AccountNotReady); } - // Get the total amount from payments - // For now, we'll use the first payment's amount - // In a multi-asset scenario, we'd need to handle this differently - let payments = info.payments; - if payments.len() == 0 { + let amount = info.payments.iter().map(|p| p.amount).sum(); + if amount == 0 { return Err(Error::AccountNotReady); } - let first_payment = payments.get(0).ok_or(Error::AccountNotReady)?; - let amount = first_payment.amount; // Execute the actual token transfer // Note: In production, the ephemeral account would need to authorize this transfer @@ -137,13 +134,13 @@ impl SweepController { /// Check if an account is ready for sweep pub fn can_sweep(env: Env, ephemeral_account: Address) -> bool { - let account_client = ephemeral_account::Client::new(&env, &ephemeral_account); + let account_client = EphemeralAccountClient::new(&env, &ephemeral_account); // Check if account exists and has payment let info = account_client.get_info(); info.payment_received - && info.status == ephemeral_account::AccountStatus::PaymentReceived + && info.status as u32 == AccountStatus::PaymentReceived as u32 && !account_client.is_expired() } @@ -158,10 +155,7 @@ impl SweepController { /// # Errors /// Returns Error::AuthorizationFailed if caller is not the creator /// Returns Error::AccountAlreadySwept if a sweep has already been executed - pub fn update_authorized_destination( - env: Env, - new_destination: Address, - ) -> Result<(), Error> { + pub fn update_authorized_destination(env: Env, new_destination: Address) -> Result<(), Error> { // Verify creator authorization let creator = storage::get_creator(&env).ok_or(Error::AuthorizationFailed)?; creator.require_auth(); @@ -236,6 +230,6 @@ fn emit_destination_updated(env: &Env, old_destination: Option
, new_des mod ephemeral_account { // Import from the actual ephemeral_account contract soroban_sdk::contractimport!( - file = "../ephemeral_account/target/wasm32-unknown-unknown/release/ephemeral_account.wasm" + file = "/home/levai/bridgelet-core/target/wasm32-unknown-unknown/release/ephemeral_account.wasm" ); } diff --git a/contracts/sweep_controller/src/storage.rs b/contracts/sweep_controller/src/storage.rs index 96d76da..12e4511 100644 --- a/contracts/sweep_controller/src/storage.rs +++ b/contracts/sweep_controller/src/storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, BytesN, Env, Address}; +use soroban_sdk::{contracttype, Address, BytesN, Env}; /// Data keys for contract storage #[contracttype] @@ -20,7 +20,9 @@ pub enum DataKey { /// * `env` - Soroban environment /// * `signer` - Ed25519 public key (32 bytes) pub fn set_authorized_signer(env: &Env, signer: &BytesN<32>) { - env.storage().instance().set(&DataKey::AuthorizedSigner, signer); + env.storage() + .instance() + .set(&DataKey::AuthorizedSigner, signer); } /// Get the authorized signer public key @@ -86,7 +88,9 @@ pub fn set_authorized_destination(env: &Env, destination: &Address) { /// # Returns /// The authorized destination address, or None if not set (flexible mode) pub fn get_authorized_destination(env: &Env) -> Option
{ - env.storage().instance().get(&DataKey::AuthorizedDestination) + env.storage() + .instance() + .get(&DataKey::AuthorizedDestination) } /// Check if an authorized destination is set diff --git a/contracts/sweep_controller/tests/integration.rs b/contracts/sweep_controller/tests/integration.rs index f9cf05b..a68e257 100644 --- a/contracts/sweep_controller/tests/integration.rs +++ b/contracts/sweep_controller/tests/integration.rs @@ -1,9 +1,9 @@ #![cfg(test)] +use ephemeral_account::{AccountStatus, EphemeralAccountContract, EphemeralAccountContractClient}; use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; -use sweep_controller::{SweepController, SweepControllerClient}; use sweep_controller::Error; -use ephemeral_account::{EphemeralAccountContract, EphemeralAccountContractClient, AccountStatus}; +use sweep_controller::{SweepController, SweepControllerClient}; /// Helper function to generate a valid Ed25519 keypair for testing /// In a real scenario, these would be generated by the off-chain system @@ -141,7 +141,7 @@ fn test_execute_sweep_with_invalid_signature() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { controller_client.execute_sweep(&ephemeral_id, &destination, &invalid_sig); })); - + // We expect this to fail assert!(result.is_err()); @@ -305,10 +305,7 @@ fn test_unauthorized_signer_not_set() { // Execute sweep without initializing controller - should fail let result = controller_client.execute_sweep(&ephemeral_id, &destination, &auth_sig); - println!( - "Execute sweep without initialization result: {:?}", - result - ); + println!("Execute sweep without initialization result: {:?}", result); } /// Test initialization with authorized destination (locked mode) @@ -488,7 +485,7 @@ fn test_update_destination_before_sweep() { // Update destination before any sweep - should succeed controller_client.update_authorized_destination(&new_dest); - + // Verify the destination was updated by trying to sweep to new destination // (The actual sweep may fail due to signature, but destination validation should pass) let ephemeral_id = env.register_contract(None, EphemeralAccountContract); @@ -503,7 +500,7 @@ fn test_update_destination_before_sweep() { ephemeral_client.record_payment(&100, &asset); let auth_sig = BytesN::from_array(&env, &[1u8; 64]); - + // Try to sweep to the new destination - destination validation should pass // (signature verification may fail, but that's expected in tests) // We're mainly checking that destination validation doesn't fail with UnauthorizedDestination @@ -515,4 +512,4 @@ fn test_update_destination_before_sweep() { // But if it's UnauthorizedDestination, that's a problem // For now, we just check it doesn't panic with UnauthorizedDestination // (In a real test, we'd check the panic message) -} \ No newline at end of file +} diff --git a/scripts/build.sh b/scripts/build.sh old mode 100644 new mode 100755