Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
17 changes: 2 additions & 15 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ mod test;

use soroban_sdk::{contract, contractimpl, contracttype, 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
29 changes: 12 additions & 17 deletions contracts/sweep_controller/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ mod errors;
mod storage;
mod transfers;

use crate::ephemeral_account::Client as EphemeralAccountClient;
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env};

use authorization::AuthContext;
use bridgelet_shared::{AccountInfo, AccountStatus};
pub use errors::Error;
use transfers::TransferContext;

#[contract]
pub struct SweepController;
Expand Down Expand Up @@ -76,8 +79,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 +99,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 +112,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 +135,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 +156,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 +231,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