Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[workspace]
resolver = "2"
members = [
"contracts/ephemeral_account",
"contracts/sweep_controller",
"contracts/shared",
]
4 changes: 3 additions & 1 deletion contracts/ephemeral_account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion contracts/ephemeral_account/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::storage::Payment;
use bridgelet_shared::Payment;
use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec};

#[contracttype]
Expand Down
19 changes: 3 additions & 16 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Payment>,
pub swept_to: Option<Address>,
}
18 changes: 1 addition & 17 deletions contracts/ephemeral_account/src/storage.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
10 changes: 10 additions & 0 deletions contracts/shared/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "bridgelet-shared"
version = "0.1.0"
edition = "2021"

[dependencies]
soroban-sdk = "22.0.0"

[lib]
crate-type = ["rlib"]
5 changes: 5 additions & 0 deletions contracts/shared/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![no_std]

mod types;

pub use types::{AccountInfo, AccountStatus, Payment};
34 changes: 34 additions & 0 deletions contracts/shared/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<Payment>,
pub swept_to: Option<Address>,
}
6 changes: 5 additions & 1 deletion contracts/sweep_controller/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
81 changes: 25 additions & 56 deletions contracts/sweep_controller/src/authorization.rs
Original file line number Diff line number Diff line change
@@ -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
///
Expand All @@ -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()
}

Expand All @@ -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();
Expand All @@ -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(())
}

Expand Down
30 changes: 12 additions & 18 deletions contracts/sweep_controller/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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()
}

Expand All @@ -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();
Expand Down Expand Up @@ -236,6 +230,6 @@ fn emit_destination_updated(env: &Env, old_destination: Option<Address>, 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"
);
}
10 changes: 7 additions & 3 deletions contracts/sweep_controller/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{contracttype, BytesN, Env, Address};
use soroban_sdk::{contracttype, Address, BytesN, Env};

/// Data keys for contract storage
#[contracttype]
Expand All @@ -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
Expand Down Expand Up @@ -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<Address> {
env.storage().instance().get(&DataKey::AuthorizedDestination)
env.storage()
.instance()
.get(&DataKey::AuthorizedDestination)
}

/// Check if an authorized destination is set
Expand Down
Loading
Loading