diff --git a/contracts/axelar-auth-verifier/src/contract.rs b/contracts/axelar-auth-verifier/src/contract.rs index 17c7214..87b6af0 100644 --- a/contracts/axelar-auth-verifier/src/contract.rs +++ b/contracts/axelar-auth-verifier/src/contract.rs @@ -70,7 +70,7 @@ impl AxelarAuthVerifierInterface for AxelarAuthVerifier { .storage() .persistent() .get(&DataKey::EpochBySignerHash(signer_set_hash)) - .unwrap(); + .unwrap_or(0); let epoch: u64 = env.storage().instance().get(&DataKey::Epoch).unwrap(); diff --git a/contracts/axelar-auth-verifier/src/test.rs b/contracts/axelar-auth-verifier/src/test.rs index a3ac947..84675c8 100644 --- a/contracts/axelar-auth-verifier/src/test.rs +++ b/contracts/axelar-auth-verifier/src/test.rs @@ -1,13 +1,16 @@ #![cfg(test)] extern crate std; -use soroban_sdk::{testutils::Address as _, xdr::ToXdr, Address, Bytes, Env}; +use soroban_sdk::{testutils::Address as _, xdr::ToXdr, Address, Bytes, Env, Vec, U256}; use axelar_soroban_std::testutils::assert_invocation; use crate::{ contract::{AxelarAuthVerifier, AxelarAuthVerifierClient}, - testutils::{generate_proof, generate_signer_set, initialize, randint, transfer_operatorship}, + testutils::{ + generate_empty_signer_set, generate_proof, generate_signer_set, initialize, randint, + transfer_operatorship, + }, }; fn setup_env<'a>() -> (Env, Address, AxelarAuthVerifierClient<'a>) { @@ -28,6 +31,65 @@ fn test_initialize() { initialize(&env, &client, user, randint(0, 10), randint(1, 10)); } +#[test] +#[should_panic(expected = "Already initialized")] +fn test_fails_if_already_initialized() { + let (env, _, client) = setup_env(); + let user_one = Address::generate(&env); + let user_two = Address::generate(&env); + + initialize(&env, &client, user_one, randint(0, 10), randint(1, 10)); + + // second initialization should panic + initialize(&env, &client, user_two, randint(0, 10), randint(1, 10)); +} + +#[test] +fn test_fails_with_empty_signer_set() { + let (env, _, client) = setup_env(); + let owner = Address::generate(&env); + + // create an empty WeightedSigners vector + let empty_signer_set = generate_empty_signer_set(&env); + + // serialize the empty signer set to Bytes + let empty_operator_set = empty_signer_set.to_xdr(&env); + + // call should panic because signer set is empty + let res = client.try_initialize(&owner, &randint(0, 10), &empty_operator_set); + assert!(res.is_err()); +} + +#[test] +fn test_transfer_ownership() { + let (env, _, client) = setup_env(); + + let initial_owner = Address::generate(&env); + let new_owner = Address::generate(&env); + + initialize( + &env, + &client, + initial_owner.clone(), + randint(1, 10), + randint(1, 10), + ); + + // transfer ownership to the new owner + client.transfer_ownership(&new_owner); + + assert_invocation( + &env, + &initial_owner, + &client.address, + "transfer_ownership", + (new_owner.clone(),), + ); + + let retrieved_owner = client.owner(); + assert_eq!(retrieved_owner, new_owner); +} + #[test] fn test_validate_proof() { let (env, _, client) = setup_env(); @@ -44,6 +106,116 @@ fn test_validate_proof() { assert!(latest_signer_set); } +#[test] +#[should_panic(expected = "invalid epoch")] +fn test_fail_validate_proof_invalid_epoch() { + let (env, _, client) = setup_env(); + let user = Address::generate(&env); + + initialize(&env, &client, user, randint(0, 10), randint(1, 10)); + + let different_signers = generate_signer_set(&env, randint(1, 10), false); + + let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); + let msg_hash = env.crypto().keccak256(&msg); + let proof = generate_proof(&env, msg_hash.clone(), different_signers); + + // should panic, epoch should return zero for invalid signer set + client.validate_proof(&msg_hash, &proof.to_xdr(&env)); +} + +#[test] +#[should_panic(expected = "invalid signatures")] +fn test_fail_validate_proof_invalid_signatures() { + let (env, _, client) = setup_env(); + let user = Address::generate(&env); + + let signers = initialize(&env, &client, user, randint(0, 10), randint(1, 10)); + + let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); + let msg_hash = env.crypto().keccak256(&msg); + let proof = generate_proof(&env, msg_hash.clone(), signers); + + let different_msg = Bytes::from_array(&env, &[0x04, 0x05, 0x06]); + let different_msg_hash = env.crypto().keccak256(&different_msg); + + // should panic, proof is for different message hash + client.validate_proof(&different_msg_hash, &proof.to_xdr(&env)); +} + +#[test] +#[should_panic(expected = "invalid signatures")] +fn test_fail_validate_proof_empty_signatures() { + let (env, _, client) = setup_env(); + let user = Address::generate(&env); + + let signers = initialize(&env, &client, user, randint(0, 10), randint(1, 10)); + + let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); + let msg_hash = env.crypto().keccak256(&msg); + let mut proof = generate_proof(&env, msg_hash.clone(), signers); + + proof.signatures = Vec::new(&env); + + // validate_proof should panic, empty signatures + client.validate_proof(&msg_hash, &proof.to_xdr(&env)); +} + +#[test] +#[should_panic(expected = "invalid signatures")] +fn test_fail_validate_proof_invalid_signer_set() { + let (env, _, client) = setup_env(); + let user = Address::generate(&env); + + let signers = initialize(&env, &client, user, randint(0, 10), randint(1, 10)); + + let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); + let msg_hash = env.crypto().keccak256(&msg); + let mut proof = generate_proof(&env, msg_hash.clone(), signers); + + let new_signers = generate_signer_set(&env, randint(1, 10), false); + let new_proof = generate_proof(&env, msg_hash.clone(), new_signers); + + proof.signatures = new_proof.signatures; + + // validate_proof should panic, signatures do not match signers + client.validate_proof(&msg_hash, &proof.to_xdr(&env)); +} + +#[test] +#[should_panic(expected = "invalid signatures")] +fn test_fail_validate_proof_threshold_not_met() { + let (env, _, client) = setup_env(); + let user = Address::generate(&env); + + let signers = initialize(&env, &client, user, randint(0, 10), randint(1, 10)); + + let env = &env; + let zero = U256::from_u32(env, 0); + let mut total_weight = zero.clone(); + + let mut index_below_threshold = 0; + + // find the index where the total weight is just below the threshold + for (i, weight) in signers.signer_set.signers.iter().map(|s| s.1).enumerate() { + total_weight = total_weight.add(&weight); + if total_weight >= signers.signer_set.threshold { + index_below_threshold = i; + break; + } + } + + let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); + let msg_hash = env.crypto().keccak256(&msg); + let mut proof = generate_proof(&env, msg_hash.clone(), signers); + + // remove signatures to just below the threshold + proof.signatures = proof.signatures.slice(0..index_below_threshold as u32); + + // should panic, all signatures are valid but total weight is below threshold + client.validate_proof(&msg_hash, &proof.to_xdr(&env)); +} + #[test] fn test_transfer_operatorship() { let (env, _, client) = setup_env(); @@ -62,7 +234,7 @@ fn test_transfer_operatorship() { let msg = Bytes::from_array(&env, &[0x01, 0x02, 0x03]); let msg_hash = env.crypto().keccak256(&msg); - let new_signers = generate_signer_set(&env, randint(1, 10)); + let new_signers = generate_signer_set(&env, randint(1, 10), false); let encoded_new_signer_set = transfer_operatorship(&env, &client, new_signers.clone()); @@ -79,6 +251,155 @@ fn test_transfer_operatorship() { assert!(latest_signer_set); } +#[test] +fn test_transfer_operatorship_fail_empty_signer_set() { + let (env, _, client) = setup_env(); + + let user = Address::generate(&env); + let previous_signer_retention = 1; + + initialize( + &env, + &client, + user.clone(), + previous_signer_retention, + randint(1, 10), + ); + + let empty_signer_set = generate_empty_signer_set(&env); + + let empty_operator_set = empty_signer_set.to_xdr(&env); + + // should throw an error, empty signer set + let res = client.try_transfer_operatorship(&empty_operator_set); + assert!(res.is_err()); +} + +#[test] +fn test_transfer_operatorship_fail_zero_weight() { + let (env, _, client) = setup_env(); + + let user = Address::generate(&env); + let previous_signer_retention = 1; + + initialize( + &env, + &client, + user.clone(), + previous_signer_retention, + randint(1, 10), + ); + + let new_signers = generate_signer_set(&env, randint(1, 10), true); + + let encoded_new_signer_set = new_signers.signer_set.to_xdr(&env); + + // should throw an error, last signer weight is zero + let res = client.try_transfer_operatorship(&encoded_new_signer_set); + assert!(res.is_err()); +} + +#[test] +fn test_transfer_operatorship_fail_zero_threshold() { + let (env, _, client) = setup_env(); + + let user = Address::generate(&env); + let previous_signer_retention = 1; + + initialize( + &env, + &client, + user.clone(), + previous_signer_retention, + randint(1, 10), + ); + + let mut new_signers = generate_signer_set(&env, randint(1, 10), false); + + // set the threshold to zero + new_signers.signer_set.threshold = U256::from_u32(&env, 0); + + let encoded_new_signer_set = new_signers.signer_set.to_xdr(&env); + + // should error because the threshold is set to zero + let res = client.try_transfer_operatorship(&encoded_new_signer_set); + assert!(res.is_err()); +} + +#[test] +fn test_transfer_operatorship_fail_low_total_weight() { + let (env, _, client) = setup_env(); + + let user = Address::generate(&env); + let previous_signer_retention = 1; + + initialize( + &env, + &client, + user.clone(), + previous_signer_retention, + randint(1, 10), + ); + + let mut new_signers = generate_signer_set(&env, randint(1, 10), false); + + let env = &env; + let zero = U256::from_u32(env, 0); + let one = U256::from_u32(env, 1); + let mut total_weight = zero.clone(); + + for weight in new_signers.signer_set.signers.iter().map(|s| s.1) { + total_weight = total_weight.add(&weight); + } + + let new_threshold = total_weight.add(&one); + + // set the threshold to zero + new_signers.signer_set.threshold = new_threshold; + + let encoded_new_signer_set = new_signers.signer_set.to_xdr(&env); + + // should error because the threshold is set to zero + let res = client.try_transfer_operatorship(&encoded_new_signer_set); + assert!(res.is_err()); +} + +#[test] +fn test_transfer_operatorship_fail_wrong_signer_order() { + let (env, _, client) = setup_env(); + + let user = Address::generate(&env); + let previous_signer_retention = 1; + + initialize( + &env, + &client, + user.clone(), + previous_signer_retention, + randint(1, 10), + ); + + let mut new_signers = generate_signer_set(&env, randint(1, 10), false); + + let len = new_signers.signer_set.signers.len(); + + // create a new vec and reverse signer order + let mut reversed_signers = Vec::new(&env); + for i in (0..len).rev() { + if let Some(item) = new_signers.signer_set.signers.get(i as u32) { + reversed_signers.push_back(item); + } + } + + new_signers.signer_set.signers = reversed_signers; + + let encoded_new_signer_set = new_signers.signer_set.to_xdr(&env); + + // should error because signers are in wrong order + let res = client.try_transfer_operatorship(&encoded_new_signer_set); + assert!(res.is_err()); +} + #[test] fn test_multi_transfer_operatorship() { let (env, _, client) = setup_env(); @@ -100,7 +421,7 @@ fn test_multi_transfer_operatorship() { let mut previous_signers = original_signers.clone(); for _ in 0..previous_signer_retention { - let new_signers = generate_signer_set(&env, randint(1, 10)); + let new_signers = generate_signer_set(&env, randint(1, 10), false); transfer_operatorship(&env, &client, new_signers.clone()); @@ -140,7 +461,7 @@ fn test_transfer_operatorship_panics_on_outdated_signer_set() { let msg_hash = env.crypto().keccak256(&msg); for _ in 0..(previous_signer_retention + 1) { - let new_signers = generate_signer_set(&env, randint(1, 10)); + let new_signers = generate_signer_set(&env, randint(1, 10), false); transfer_operatorship(&env, &client, new_signers.clone()); } diff --git a/contracts/axelar-auth-verifier/src/testutils.rs b/contracts/axelar-auth-verifier/src/testutils.rs index 5f8a5ad..ecc033e 100644 --- a/contracts/axelar-auth-verifier/src/testutils.rs +++ b/contracts/axelar-auth-verifier/src/testutils.rs @@ -9,7 +9,7 @@ use rand::rngs::OsRng; use rand::Rng; use secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; use sha3::{Digest, Keccak256}; -use soroban_sdk::{vec, U256}; +use soroban_sdk::{vec, Vec, U256}; use soroban_sdk::{symbol_short, testutils::Events, xdr::ToXdr, Address, Bytes, BytesN, Env}; @@ -25,7 +25,11 @@ pub fn randint(a: u32, b: u32) -> u32 { rand::thread_rng().gen_range(a..b) } -pub fn generate_signer_set(env: &Env, num_signers: u32) -> TestSignerSet { +pub fn generate_signer_set( + env: &Env, + num_signers: u32, + include_zero_weight: bool, +) -> TestSignerSet { let secp = Secp256k1::new(); let mut rng = rand::thread_rng(); @@ -40,6 +44,13 @@ pub fn generate_signer_set(env: &Env, num_signers: u32) -> TestSignerSet { }) .collect(); + if include_zero_weight { + let sk = SecretKey::new(&mut OsRng); + let pk = PublicKey::from_secret_key(&secp, &sk); + let pk_hash: [u8; 32] = Keccak256::digest(&pk.serialize_uncompressed()).into(); + signer_keypair.push((sk, (pk, pk_hash, 0))); + } + // Sort signers by public key hash signer_keypair.sort_by(|(_, (_, h1, _)), (_, (_, h2, _))| h1.cmp(h2)); @@ -70,6 +81,10 @@ pub fn generate_signer_set(env: &Env, num_signers: u32) -> TestSignerSet { } } +pub fn generate_empty_signer_set(env: &Env) -> Vec { + Vec::::new(&env) +} + pub fn generate_proof(env: &Env, msg_hash: BytesN<32>, signers: TestSignerSet) -> Proof { let msg = Message::from_digest_slice(&msg_hash.to_array()).unwrap(); let threshold = signers.signer_set.threshold.to_u128().unwrap() as u32; @@ -104,7 +119,7 @@ pub fn initialize( previous_signer_retention: u32, num_signers: u32, ) -> TestSignerSet { - let signers = generate_signer_set(env, num_signers); + let signers = generate_signer_set(env, num_signers, false); let signer_sets = vec![&env, signers.signer_set.clone()].to_xdr(env); let signer_set_hash = env .crypto()