Skip to content

Commit

Permalink
feat(minor-voting-verifier): validate source address (#545)
Browse files Browse the repository at this point in the history
  • Loading branch information
haiyizxx authored Jul 29, 2024
1 parent aba3433 commit 3a64ad6
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 37 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contracts/voting-verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ service-registry = { workspace = true, features = ["library"] }
thiserror = { workspace = true }

[dev-dependencies]
alloy-primitives = { version = "0.7.7", features = ["getrandom"] }
cw-multi-test = "0.15.1"
integration-tests = { workspace = true }
multisig = { workspace = true, features = ["test", "library"] }
Expand Down
1 change: 1 addition & 0 deletions contracts/voting-verifier/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ mod test {
source_chain: "source-chain".parse().unwrap(),
rewards_address: "rewards".try_into().unwrap(),
msg_id_format: axelar_wasm_std::msg_id::MessageIdFormat::HexTxHashAndEventIndex,
address_format: axelar_wasm_std::address_format::AddressFormat::Eip55,
};

instantiate(deps, env, info.clone(), msg.clone()).unwrap();
Expand Down
85 changes: 74 additions & 11 deletions contracts/voting-verifier/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use axelar_wasm_std::{permission_control, FnExt};
use axelar_wasm_std::permission_control;
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
Expand Down Expand Up @@ -39,6 +39,7 @@ pub fn instantiate(
source_chain: msg.source_chain,
rewards_contract: deps.api.addr_validate(&msg.rewards_address)?,
msg_id_format: msg.msg_id_format,
address_format: msg.address_format,
};
CONFIG.save(deps.storage, &config)?;

Expand All @@ -54,18 +55,25 @@ pub fn execute(
msg: ExecuteMsg,
) -> Result<Response, axelar_wasm_std::error::ContractError> {
match msg.ensure_permissions(deps.storage, &info.sender)? {
ExecuteMsg::VerifyMessages(messages) => execute::verify_messages(deps, env, messages),
ExecuteMsg::Vote { poll_id, votes } => execute::vote(deps, env, info, poll_id, votes),
ExecuteMsg::EndPoll { poll_id } => execute::end_poll(deps, env, poll_id),
ExecuteMsg::VerifyMessages(messages) => Ok(execute::verify_messages(deps, env, messages)?),
ExecuteMsg::Vote { poll_id, votes } => Ok(execute::vote(deps, env, info, poll_id, votes)?),
ExecuteMsg::EndPoll { poll_id } => Ok(execute::end_poll(deps, env, poll_id)?),
ExecuteMsg::VerifyVerifierSet {
message_id,
new_verifier_set,
} => execute::verify_verifier_set(deps, env, &message_id, new_verifier_set),
} => Ok(execute::verify_verifier_set(
deps,
env,
&message_id,
new_verifier_set,
)?),
ExecuteMsg::UpdateVotingThreshold {
new_voting_threshold,
} => execute::update_voting_threshold(deps, new_voting_threshold),
}?
.then(Ok)
} => Ok(execute::update_voting_threshold(
deps,
new_voting_threshold,
)?),
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand Down Expand Up @@ -98,12 +106,15 @@ pub fn migrate(

#[cfg(test)]
mod test {
use axelar_wasm_std::address_format::AddressFormat;
use axelar_wasm_std::msg_id::{
Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, HexTxHashAndEventIndex,
MessageIdFormat,
};
use axelar_wasm_std::voting::Vote;
use axelar_wasm_std::{nonempty, MajorityThreshold, Threshold, VerificationStatus};
use axelar_wasm_std::{
err_contains, nonempty, MajorityThreshold, Threshold, VerificationStatus,
};
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage,
};
Expand Down Expand Up @@ -179,6 +190,7 @@ mod test {
source_chain: source_chain(),
rewards_address: REWARDS_ADDRESS.parse().unwrap(),
msg_id_format: msg_id_format.clone(),
address_format: AddressFormat::Eip55,
},
)
.unwrap();
Expand Down Expand Up @@ -237,7 +249,10 @@ mod test {
.map(|i| Message {
cc_id: CrossChainId::new(source_chain(), message_id("id", i, msg_id_format))
.unwrap(),
source_address: format!("source-address{i}").parse().unwrap(),
source_address: alloy_primitives::Address::random()
.to_string()
.try_into()
.unwrap(),
destination_chain: format!("destination-chain{i}").parse().unwrap(),
destination_address: format!("destination-address{i}").parse().unwrap(),
payload_hash: [0; 32],
Expand Down Expand Up @@ -269,7 +284,10 @@ mod test {
Message {
cc_id: CrossChainId::new(source_chain(), message_id("id", 1, &msg_id_format))
.unwrap(),
source_address: "source-address1".parse().unwrap(),
source_address: alloy_primitives::Address::random()
.to_string()
.parse()
.unwrap(),
destination_chain: "destination-chain1".parse().unwrap(),
destination_address: "destination-address1".parse().unwrap(),
payload_hash: [0; 32],
Expand Down Expand Up @@ -1350,4 +1368,49 @@ mod test {
}
});
}

#[test]
fn should_fail_if_messages_have_invalid_source_address() {
let msg_id_format = MessageIdFormat::HexTxHashAndEventIndex;
let verifiers = verifiers(2);
let mut deps = setup(verifiers.clone(), &msg_id_format);

let eip55_address = alloy_primitives::Address::random().to_string();

let cases = vec![
eip55_address.to_lowercase(),
eip55_address.to_uppercase(),
// mix
eip55_address
.chars()
.enumerate()
.map(|(i, c)| {
if i % 2 == 0 {
c.to_uppercase().next().unwrap()
} else {
c.to_lowercase().next().unwrap()
}
})
.collect::<String>(),
];

for case in cases {
let mut messages = messages(1, &MessageIdFormat::HexTxHashAndEventIndex);
messages[0].source_address = case.parse().unwrap();
let msg = ExecuteMsg::VerifyMessages(messages);
let res = execute(deps.as_mut(), mock_env(), mock_info(SENDER, &[]), msg);
assert!(res.is_err_and(|err| err_contains!(
err.report,
ContractError,
ContractError::InvalidSourceAddress { .. }
)));
}

// should not fail if address is valid
let mut messages = messages(1, &MessageIdFormat::HexTxHashAndEventIndex);
messages[0].source_address = eip55_address.parse().unwrap();
let msg = ExecuteMsg::VerifyMessages(messages);
let res = execute(deps.as_mut(), mock_env(), mock_info(SENDER, &[]), msg);
assert!(res.is_ok());
}
}
69 changes: 45 additions & 24 deletions contracts/voting-verifier/src/contract/execute.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::collections::HashMap;

use axelar_wasm_std::address_format::{validate_address, AddressFormat};
use axelar_wasm_std::utils::TryMapExt;
use axelar_wasm_std::voting::{PollId, PollResults, Vote, WeightedPoll};
use axelar_wasm_std::{snapshot, MajorityThreshold, VerificationStatus};
use cosmwasm_std::{
to_json_binary, Deps, DepsMut, Env, Event, MessageInfo, OverflowError, OverflowOperation,
QueryRequest, Response, Storage, WasmMsg, WasmQuery,
};
use error_stack::{report, Report, ResultExt};
use itertools::Itertools;
use multisig::verifier_set::VerifierSet;
use router_api::{ChainName, Message};
Expand Down Expand Up @@ -81,29 +84,22 @@ pub fn verify_messages(
deps: DepsMut,
env: Env,
messages: Vec<Message>,
) -> Result<Response, ContractError> {
) -> Result<Response, Report<ContractError>> {
if messages.is_empty() {
Err(ContractError::EmptyMessages)?;
}

let source_chain = CONFIG.load(deps.storage)?.source_chain;
let config = CONFIG.load(deps.storage).map_err(ContractError::from)?;

if messages
.iter()
.any(|message| message.cc_id.source_chain != source_chain)
{
Err(ContractError::SourceChainMismatch(source_chain.clone()))?;
}

let config = CONFIG.load(deps.storage)?;

let messages = messages
.into_iter()
.map(|message| {
message_status(deps.as_ref(), &message, env.block.height)
.map(|status| (status, message))
})
.collect::<Result<Vec<_>, _>>()?;
let messages = messages.try_map(|message| {
validate_source_chain(message, &config.source_chain)
.and_then(|message| validate_source_address(message, &config.address_format))
.and_then(|message| {
message_status(deps.as_ref(), &message, env.block.height)
.map(|status| (status, message))
.map_err(Report::from)
})
})?;

let msgs_to_verify: Vec<Message> = messages
.into_iter()
Expand All @@ -121,18 +117,20 @@ pub fn verify_messages(
return Ok(Response::new());
}

let snapshot = take_snapshot(deps.as_ref(), &source_chain)?;
let snapshot = take_snapshot(deps.as_ref(), &config.source_chain)?;
let participants = snapshot.participants();
let expires_at = calculate_expiration(env.block.height, config.block_expiry.into())?;

let id = create_messages_poll(deps.storage, expires_at, snapshot, msgs_to_verify.len())?;

for (idx, message) in msgs_to_verify.iter().enumerate() {
poll_messages().save(
deps.storage,
&message.hash(),
&state::PollContent::<Message>::new(message.clone(), id, idx),
)?;
poll_messages()
.save(
deps.storage,
&message.hash(),
&state::PollContent::<Message>::new(message.clone(), id, idx),
)
.map_err(ContractError::from)?;
}

let messages = msgs_to_verify
Expand Down Expand Up @@ -366,3 +364,26 @@ fn calculate_expiration(block_height: u64, block_expiry: u64) -> Result<u64, Con
.ok_or_else(|| OverflowError::new(OverflowOperation::Add, block_height, block_expiry))
.map_err(ContractError::from)
}

fn validate_source_chain(
message: Message,
source_chain: &ChainName,
) -> Result<Message, Report<ContractError>> {
if message.cc_id.source_chain != *source_chain {
Err(report!(ContractError::SourceChainMismatch(
source_chain.clone()
)))
} else {
Ok(message)
}
}

fn validate_source_address(
message: Message,
address_format: &AddressFormat,
) -> Result<Message, Report<ContractError>> {
validate_address(&message.source_address, address_format)
.change_context(ContractError::InvalidSourceAddress)?;

Ok(message)
}
2 changes: 2 additions & 0 deletions contracts/voting-verifier/src/contract/migrations/v0_5_0.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(deprecated)]

use axelar_wasm_std::address_format::AddressFormat;
use axelar_wasm_std::error::ContractError;
use axelar_wasm_std::msg_id::MessageIdFormat;
use axelar_wasm_std::{nonempty, permission_control, MajorityThreshold};
Expand Down Expand Up @@ -43,6 +44,7 @@ fn migrate_config(storage: &mut dyn Storage, config: Config) -> StdResult<()> {
msg_id_format: config.msg_id_format,
source_gateway_address: config.source_gateway_address,
voting_threshold: config.voting_threshold,
address_format: AddressFormat::Eip55,
};

state::CONFIG.save(storage, &config)
Expand Down
3 changes: 3 additions & 0 deletions contracts/voting-verifier/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub enum ContractError {

#[error("poll results have different length")]
PollResultsLengthUnequal,

#[error("invalid source address")]
InvalidSourceAddress,
}

impl From<ContractError> for StdError {
Expand Down
5 changes: 5 additions & 0 deletions contracts/voting-verifier/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl From<Config> for Vec<Attribute> {
source_chain,
rewards_contract,
msg_id_format,
address_format,
} = other;

vec![
Expand All @@ -50,6 +51,10 @@ impl From<Config> for Vec<Attribute> {
"msg_id_format",
serde_json::to_string(&msg_id_format).expect("failed to serialize msg_id_format"),
),
(
"address_format",
serde_json::to_string(&address_format).expect("failed to serialize address_format"),
),
]
.into_iter()
.map(Attribute::from)
Expand Down
2 changes: 2 additions & 0 deletions contracts/voting-verifier/src/msg.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use axelar_wasm_std::address_format::AddressFormat;
use axelar_wasm_std::msg_id::MessageIdFormat;
use axelar_wasm_std::voting::{PollId, PollStatus, Vote, WeightedPoll};
use axelar_wasm_std::{nonempty, MajorityThreshold, VerificationStatus};
Expand Down Expand Up @@ -30,6 +31,7 @@ pub struct InstantiateMsg {
pub rewards_address: nonempty::String,
/// Format that incoming messages should use for the id field of CrossChainId
pub msg_id_format: MessageIdFormat,
pub address_format: AddressFormat,
}

#[cw_serde]
Expand Down
2 changes: 2 additions & 0 deletions contracts/voting-verifier/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use axelar_wasm_std::address_format::AddressFormat;
use axelar_wasm_std::hash::Hash;
use axelar_wasm_std::msg_id::MessageIdFormat;
use axelar_wasm_std::voting::{PollId, Vote, WeightedPoll};
Expand All @@ -21,6 +22,7 @@ pub struct Config {
pub source_chain: ChainName,
pub rewards_contract: Addr,
pub msg_id_format: MessageIdFormat,
pub address_format: AddressFormat,
}

#[cw_serde]
Expand Down
1 change: 1 addition & 0 deletions integration-tests/src/voting_verifier_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl VotingVerifierContract {
.try_into()
.unwrap(),
msg_id_format: axelar_wasm_std::msg_id::MessageIdFormat::HexTxHashAndEventIndex,
address_format: axelar_wasm_std::address_format::AddressFormat::Eip55,
},
&[],
"voting_verifier",
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/tests/chain_freeze_unfreeze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn chain_can_be_freezed_unfreezed() {
"0x88d7956fd7b6fcec846548d83bd25727f2585b4be3add21438ae9fbb34625924-3",
)
.unwrap(),
source_address: "0xBf12773B49()0e1Deb57039061AAcFA2A87DEaC9b9"
source_address: "0xBf12773B490e1Deb57039061AAcFA2A87DEaC9b9"
.to_string()
.try_into()
.unwrap(),
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/tests/message_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn single_message_can_be_verified_and_routed_and_proven_and_rewards_are_distribu
"0x88d7956fd7b6fcec846548d83bd25727f2585b4be3add21438ae9fbb34625924-3",
)
.unwrap(),
source_address: "0xBf12773B49()0e1Deb57039061AAcFA2A87DEaC9b9"
source_address: "0xBf12773B490e1Deb57039061AAcFA2A87DEaC9b9"
.to_string()
.try_into()
.unwrap(),
Expand Down
1 change: 1 addition & 0 deletions packages/axelar-wasm-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \
"""

[dependencies]
alloy-primitives = { workspace = true }
bs58 = "0.5.1"
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
Expand Down
Loading

0 comments on commit 3a64ad6

Please sign in to comment.