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
24 changes: 12 additions & 12 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,53 @@ on:
jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
override: true

- name: Install soroban-cli
run: cargo install --locked soroban-cli --version 22.0.0

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}

- name: Cache target directory
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-target-${{ hashFiles('**/Cargo.lock') }}

- name: Run tests
run: |
cd contracts/ephemeral_account
cargo test --verbose

- name: Check format
run: |
cd contracts/ephemeral_account
cargo fmt -- --check

- name: Run clippy
run: |
cd contracts/ephemeral_account
cargo clippy -- -D warnings

- name: Build all contracts
run: |
cd contracts/ephemeral_account
Expand All @@ -64,7 +64,7 @@ jobs:
cargo build --target wasm32-unknown-unknown --release
cd ../reserve_contract
cargo build --target wasm32-unknown-unknown --release

- name: Upload WASM artifacts
uses: actions/upload-artifact@v4
with:
Expand All @@ -73,4 +73,4 @@ jobs:
contracts/ephemeral_account/target/wasm32-unknown-unknown/release/*.wasm
contracts/sweep_controller/target/wasm32-unknown-unknown/release/*.wasm
contracts/reserve_contract/target/wasm32-unknown-unknown/release/*.wasm
retention-days: 30
retention-days: 30
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Bridgelet Core contains the Soroban smart contracts that enforce single-use rest
### Implementation Notes

- **EphemeralAccount::sweep()**: Currently uses Soroban's `require_auth()` for authorization instead of cryptographic Ed25519 signature verification. The signature parameters (`destination`, `auth_signature`) are accepted but not cryptographically verified. Production implementation should use `env.crypto().ed25519_verify()` similar to SweepController's implementation.
- **SweepController::claim()**: Experimental gas-free claim path. The recipient signs a Soroban auth entry for `claim(recipient, ephemeral_account)`, and a relayer/SDK can submit the transaction and pay fees. Internally the controller uses `authorize_as_current_contract()` so the downstream `EphemeralAccount::sweep()` call can satisfy `authorized_controller.require_auth()`.
- **SweepController::execute_transfers()**: Token transfer logic is fully implemented using SEP-41 token contracts. All recorded payments are transferred atomically to the destination.
- **Security guidance**: Always route sweeps through `SweepController` for proper Ed25519 signature verification. Do not call `EphemeralAccount::sweep()` directly until the signature verification stub is replaced.

Expand Down Expand Up @@ -172,4 +173,4 @@ See [Security Audit Report](./docs/security-audit.md) (coming soon)

## License

MIT
MIT
9 changes: 6 additions & 3 deletions contracts/account_factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use bridgelet_shared::{AccountInitRequest, AccountInitResult};
use ephemeral_account::EphemeralAccountContractClient as EphemeralAccountClient;
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Vec};
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec};

#[contract]
pub struct AccountFactory;
Expand Down Expand Up @@ -45,11 +45,13 @@ impl AccountFactory {

for (index, request) in requests.iter().enumerate() {
// Deploy a new ephemeral account contract with unique salt
let salt = BytesN::from_array(&env, &(index as u32).to_be_bytes());
let mut salt_bytes = [0u8; 32];
salt_bytes[28..32].copy_from_slice(&(index as u32).to_be_bytes());
let salt = BytesN::from_array(&env, &salt_bytes);
let account_address = env
.deployer()
.with_current_contract(salt)
.deploy(&wasm_hash);
.deploy_v2(wasm_hash.clone(), ());

// Initialize it
let client = EphemeralAccountClient::new(&env, &account_address);
Expand All @@ -58,6 +60,7 @@ impl AccountFactory {
&creator,
&request.expiry_ledger,
&request.recovery_address,
&creator,
) {
Ok(_) => AccountInitResult {
account_address: account_address.clone(),
Expand Down
4 changes: 2 additions & 2 deletions contracts/account_factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn test_batch_initialize_flow() {

// Deploy the ephemeral account contract to get its wasm (this is how you get wasm in tests)
let ephemeral_account_template = env.register_contract(None, EphemeralAccountContract);

// Get the wasm hash from the registered contract
let wasm_hash = env.deployer().update_current_contract_wasm(ephemeral_account_template.clone());

Expand All @@ -36,7 +36,7 @@ fn test_batch_initialize_flow() {
AccountInitRequest { expiry_ledger: expiry + 500, recovery_address: recovery2.clone() },
];

// We can't fully test deployment from within a test like this because
// We can't fully test deployment from within a test like this because
// the deployer API in test is limited, but we have verified the flow!
// The key takeaway is that Soroban's deployer API allows contract-to-contract deployment!

Expand Down
9 changes: 5 additions & 4 deletions contracts/ephemeral_account/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl EphemeralAccountContract {
creator: Address,
expiry_ledger: u32,
recovery_address: Address,
authorized_controller: Address,
) -> Result<(), Error> {
// Check if already initialized
if storage::is_initialized(&env) {
Expand All @@ -58,6 +59,7 @@ impl EphemeralAccountContract {
storage::set_expiry_ledger(&env, expiry_ledger);
storage::set_recovery_address(&env, &recovery_address);
storage::set_status(&env, AccountStatus::Active);
storage::set_authorized_controller(&env, &authorized_controller);
storage::init_reserve_tracking(&env, BASE_RESERVE_STROOPS);

// Emit event
Expand Down Expand Up @@ -353,13 +355,12 @@ impl EphemeralAccountContract {
// Private helper functions

fn verify_sweep_authorization(
_env: &Env,
env: &Env,
_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
let controller = storage::get_authorized_controller(env).ok_or(Error::Unauthorized)?;
controller.require_auth();
Ok(())
}

Expand Down
12 changes: 12 additions & 0 deletions contracts/ephemeral_account/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum DataKey {
LastSweepId,
ReserveEventCount,
LastReserveEvent,
AuthorizedController,
}

// Initialization
Expand Down Expand Up @@ -204,3 +205,14 @@ pub fn set_last_reserve_event(env: &Env, event: &ReserveReclaimed) {
pub fn get_last_reserve_event(env: &Env) -> Option<ReserveReclaimed> {
env.storage().instance().get(&DataKey::LastReserveEvent)
}

// Authorized controller
pub fn set_authorized_controller(env: &Env, controller: &Address) {
env.storage()
.instance()
.set(&DataKey::AuthorizedController, controller);
}

pub fn get_authorized_controller(env: &Env) -> Option<Address> {
env.storage().instance().get(&DataKey::AuthorizedController)
}
Loading
Loading