diff --git a/Cargo.toml b/Cargo.toml index 5b7cdaf..b022a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,6 @@ members = [ "contracts/shared", "contracts/reserve_contract", "contracts/native_transfer", + "contracts/claim_verifier", + ] diff --git a/contracts/claim_verifier/Cargo.toml b/contracts/claim_verifier/Cargo.toml new file mode 100644 index 0000000..cc4c9ea --- /dev/null +++ b/contracts/claim_verifier/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "claim-verifier" +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/claim_verifier/src/errors.rs b/contracts/claim_verifier/src/errors.rs new file mode 100644 index 0000000..0875e13 --- /dev/null +++ b/contracts/claim_verifier/src/errors.rs @@ -0,0 +1,11 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + AuthorizedSignerNotSet = 3, + InvalidSignature = 4, + SignatureVerificationFailed = 5, +} diff --git a/contracts/claim_verifier/src/event.rs b/contracts/claim_verifier/src/event.rs new file mode 100644 index 0000000..cfeda94 --- /dev/null +++ b/contracts/claim_verifier/src/event.rs @@ -0,0 +1,14 @@ +use soroban_sdk::{contracttype, symbol_short, Address, Env}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerificationSucceeded { + pub destination: Address, + pub nonce: u64, +} + +pub fn emit_verification_succeeded(env: &Env, destination: Address, nonce: u64) { + let event = VerificationSucceeded { destination, nonce }; + env.events() + .publish((symbol_short!("verified"),), event); +} diff --git a/contracts/claim_verifier/src/lib.rs b/contracts/claim_verifier/src/lib.rs new file mode 100644 index 0000000..a411ed9 --- /dev/null +++ b/contracts/claim_verifier/src/lib.rs @@ -0,0 +1,101 @@ +#![no_std] + +mod errors; +mod events; + +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; + +pub use errors::Error; +pub use events::VerificationSucceeded; + +#[contract] +pub struct ClaimVerifierContract; + +#[contractimpl] +impl ClaimVerifierContract { + /// Initialize with the authorized signer public key + /// + /// # Arguments + /// * `authorized_signer` - Ed25519 public key (32 bytes) + /// + /// # Errors + /// * `Error::AlreadyInitialized` - called more than once + pub fn initialize(env: Env, authorized_signer: BytesN<32>) -> Result<(), Error> { + if env.storage().instance().has(&"signer") { + return Err(Error::AlreadyInitialized); + } + + env.storage() + .instance() + .set(&"signer", &authorized_signer); + + Ok(()) + } + + /// Verify an Ed25519 sweep authorization signature + /// + /// Message format matches sweep_controller/authorization.rs: + /// hash(destination + nonce + contract_id) + /// + /// # Arguments + /// * `destination` - Destination wallet address + /// * `nonce` - Current sweep nonce + /// * `signature` - Ed25519 signature (64 bytes) + /// + /// # Errors + /// * `Error::NotInitialized` - contract not initialized + /// * `Error::AuthorizedSignerNotSet` - no signer stored + /// * `Error::SignatureVerificationFailed` - signature is invalid + pub fn verify( + env: Env, + destination: Address, + nonce: u64, + signature: BytesN<64>, + ) -> Result<(), Error> { + // Get authorized signer + let authorized_signer: BytesN<32> = env + .storage() + .instance() + .get(&"signer") + .ok_or(Error::AuthorizedSignerNotSet)?; + + // Construct message: hash(destination + nonce + contract_id) + let message = Self::construct_message(&env, &destination, nonce); + + // Verify Ed25519 signature + env.crypto() + .ed25519_verify(&authorized_signer, &message.into(), &signature); + + // Emit success event + events::emit_verification_succeeded(&env, destination, nonce); + + Ok(()) + } + + // Private helper — constructs the message hash identical to + // sweep_controller/authorization.rs construct_sweep_message + fn construct_message(env: &Env, destination: &Address, nonce: u64) -> BytesN<32> { + use soroban_sdk::xdr::ToXdr; + + let contract_id = env.current_contract_address(); + let mut message = soroban_sdk::Bytes::new(env); + + let dest_bytes = destination.to_xdr(env); + message.append(&dest_bytes); + + 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); + + let contract_bytes = contract_id.to_xdr(env); + message.append(&contract_bytes); + + env.crypto().sha256(&message).into() + } +} + diff --git a/contracts/sweep_controller/Cargo.toml b/contracts/sweep_controller/Cargo.toml index f5c4b97..86b8a70 100644 --- a/contracts/sweep_controller/Cargo.toml +++ b/contracts/sweep_controller/Cargo.toml @@ -8,15 +8,18 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "22.0.0" +native-transfer = { path = "../native_transfer" } bridgelet-shared = { path = "../shared", version = "0.1.0" } ephemeral_account = { path = "../ephemeral_account", version = "0.1.0" } +claim-verifier = { path = "../claim_verifier" } soroban-token-sdk = "22.0.0" [dev-dependencies] soroban-sdk = { version = "22.0.0", features = ["testutils"] } bridgelet-shared = { path = "../shared", features = ["testutils"] } - +native-transfer = { path = "../native_transfer", features = ["testutils"] } +claim-verifier = { path = "../claim_verifier", features = ["testutils"] } [profile.release] opt-level = "z" @@ -31,3 +34,4 @@ lto = true [profile.release-with-logs] inherits = "release" debug-assertions = true + diff --git a/contracts/sweep_controller/src/lib.rs b/contracts/sweep_controller/src/lib.rs index 068dcb6..e40a443 100644 --- a/contracts/sweep_controller/src/lib.rs +++ b/contracts/sweep_controller/src/lib.rs @@ -1,4 +1,7 @@ #![no_std] +use claim_verifier::ClaimVerifierContractClient as ClaimVerifierClient; + + mod authorization; mod errors; @@ -31,6 +34,9 @@ impl SweepController { authorized_signer: BytesN<32>, authorized_destination: Option
, creator: Address, + native_transfer_address: Address, + native_asset_address: Address, + claim_verifier_address: Address, ) -> Result<(), Error> { // Check if already initialized if storage::get_authorized_signer(&env).is_some() { @@ -47,6 +53,10 @@ impl SweepController { // Initialize the sweep nonce to 0 storage::init_sweep_nonce(&env); + storage::set_native_transfer_address(&env, &native_transfer_address); + storage::set_native_asset_address(&env, &native_asset_address); + storage::set_claim_verifier_address(&env, &claim_verifier_address); + // Store authorized destination if provided if let Some(destination) = authorized_destination { @@ -84,17 +94,17 @@ impl SweepController { } } - // Verify authorization - let auth_ctx = AuthContext::new( - ephemeral_account.clone(), - destination.clone(), - auth_signature.clone(), - ); - auth_ctx.verify(&env)?; + // Verify authorization via claim_verifier contract + let claim_verifier_address = storage::get_claim_verifier_address(&env) + .ok_or(Error::AuthorizationFailed)?; + let verifier = ClaimVerifierClient::new(&env, &claim_verifier_address); + let nonce = storage::get_sweep_nonce(&env); + verifier.verify(&destination, &nonce, &auth_signature); - // Increment nonce after successful verification to prevent replay attacks + // Increment nonce after successful verification authorization::increment_nonce(&env); + // Call ephemeral account contract to validate and authorize sweep // This triggers the account's sweep() method which updates state let account_client = EphemeralAccountClient::new(&env, &ephemeral_account); diff --git a/contracts/sweep_controller/src/storage.rs b/contracts/sweep_controller/src/storage.rs index 12e4511..ee7384d 100644 --- a/contracts/sweep_controller/src/storage.rs +++ b/contracts/sweep_controller/src/storage.rs @@ -12,6 +12,9 @@ pub enum DataKey { AuthorizedDestination, /// Creator address (the address that initialized the contract) Creator, + NativeTransferAddress, + NativeAssetAddress, + ClaimVerifierAddress, } /// Set the authorized signer public key @@ -125,3 +128,16 @@ pub fn set_creator(env: &Env, creator: &Address) { pub fn get_creator(env: &Env) -> Option { env.storage().instance().get(&DataKey::Creator) } + +pub fn set_claim_verifier_address(env: &Env, address: &Address) { + env.storage() + .instance() + .set(&DataKey::ClaimVerifierAddress, address); +} + +pub fn get_claim_verifier_address(env: &Env) -> Option { + env.storage() + .instance() + .get(&DataKey::ClaimVerifierAddress) +} +