diff --git a/contracts/pyth_vaa/src/contract.rs b/contracts/pyth_vaa/src/contract.rs index f145f8efa..1659f8817 100644 --- a/contracts/pyth_vaa/src/contract.rs +++ b/contracts/pyth_vaa/src/contract.rs @@ -1,11 +1,17 @@ use cosmwasm_std::{ entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + Storage, }; use crate::error::ContractError; -use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::router; -use crate::state::{Config, CONFIG}; +use crate::state::{ + Config, RouterSet, RouterState, RouterVerifierConfig, CONFIG, LEGACY_CONFIG, ROUTER_SETS, + ROUTER_STATE, +}; + +const DEFAULT_GOVERNANCE_TARGET_CHAIN: u16 = 0; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -21,7 +27,24 @@ pub fn instantiate( deps.storage, &Config { admin, - router_verifier, + governance_target_chain: msg + .governance_target_chain + .unwrap_or(DEFAULT_GOVERNANCE_TARGET_CHAIN), + expected_emitter_chain: router_verifier.expected_emitter_chain, + expected_emitter_address: router_verifier.expected_emitter_address, + }, + )?; + ROUTER_STATE.save( + deps.storage, + &RouterState { + router_set_index: router_verifier.router_set_index, + }, + )?; + ROUTER_SETS.save( + deps.storage, + router_verifier.router_set_index, + &RouterSet { + routers: router_verifier.routers, }, )?; @@ -31,6 +54,37 @@ pub fn instantiate( .add_attribute("router_verifier", "configured")) } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let legacy = LEGACY_CONFIG.load(deps.storage)?; + let router_verifier = legacy.router_verifier; + let router_set_index = router_verifier.router_set_index; + + CONFIG.save( + deps.storage, + &Config { + admin: legacy.admin, + governance_target_chain: msg + .governance_target_chain + .unwrap_or(DEFAULT_GOVERNANCE_TARGET_CHAIN), + expected_emitter_chain: router_verifier.expected_emitter_chain, + expected_emitter_address: router_verifier.expected_emitter_address, + }, + )?; + ROUTER_STATE.save(deps.storage, &RouterState { router_set_index })?; + ROUTER_SETS.save( + deps.storage, + router_set_index, + &RouterSet { + routers: router_verifier.routers, + }, + )?; + + Ok(Response::new() + .add_attribute("method", "migrate") + .add_attribute("router_set_index", router_set_index.to_string())) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -40,9 +94,7 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::TransferAdmin { new_admin } => execute_transfer_admin(deps, info, new_admin), - ExecuteMsg::UpdateConfig { router_verifier } => { - execute_update_config(deps, info, router_verifier) - } + ExecuteMsg::SubmitVAA { vaa } => execute_submit_vaa(deps, vaa), } } @@ -64,22 +116,43 @@ fn execute_transfer_admin( .add_attribute("new_admin", new_admin)) } -fn execute_update_config( - deps: DepsMut, - info: MessageInfo, - router_verifier: crate::msg::RouterVerifierConfigMsg, -) -> Result { - let mut config = CONFIG.load(deps.storage)?; - if info.sender != config.admin { - return Err(ContractError::Unauthorized {}); +fn execute_submit_vaa(deps: DepsMut, vaa: Binary) -> Result { + let config = CONFIG.load(deps.storage)?; + let mut router_state = ROUTER_STATE.load(deps.storage)?; + let current_router_verifier = load_router_verifier(deps.storage)?; + let parsed_vaa = router::verify_vaa(¤t_router_verifier, vaa.as_slice())?; + let router_set_update = + router::parse_router_set_update(&parsed_vaa.payload, config.governance_target_chain)?; + let expected_next_index = router_state + .router_set_index + .checked_add(1) + .ok_or(ContractError::RouterSetIndexIncreaseError)?; + + if router_set_update.router_set_index != expected_next_index { + return Err(ContractError::RouterSetIndexIncreaseError); + } + if ROUTER_SETS.has(deps.storage, router_set_update.router_set_index) { + return Err(ContractError::RouterSetAlreadyExists); } - config.router_verifier = router::parse_config(router_verifier)?; - CONFIG.save(deps.storage, &config)?; + let old_router_set_index = router_state.router_set_index; + router_state.router_set_index = router_set_update.router_set_index; + ROUTER_SETS.save( + deps.storage, + router_set_update.router_set_index, + &RouterSet { + routers: router_set_update.routers, + }, + )?; + ROUTER_STATE.save(deps.storage, &router_state)?; Ok(Response::new() - .add_attribute("method", "update_config") - .add_attribute("router_verifier", "configured")) + .add_attribute("method", "submit_v_a_a") + .add_attribute("old_router_set_index", old_router_set_index.to_string()) + .add_attribute( + "new_router_set_index", + router_state.router_set_index.to_string(), + )) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -91,16 +164,31 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } fn verify_vaa(deps: Deps, vaa: Binary) -> Result { - let config = CONFIG.load(deps.storage)?; - router::verify_vaa(&config.router_verifier, vaa.as_slice()) + let config = load_router_verifier(deps.storage)?; + router::verify_vaa(&config, vaa.as_slice()) } fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; + let router_verifier = load_router_verifier(deps.storage)?; Ok(ConfigResponse { admin: config.admin.to_string(), - router_verifier: router::config_to_msg(&config.router_verifier), + governance_target_chain: config.governance_target_chain, + router_verifier: router::config_to_msg(&router_verifier), + }) +} + +fn load_router_verifier(storage: &dyn Storage) -> Result { + let config = CONFIG.load(storage)?; + let router_state = ROUTER_STATE.load(storage)?; + let router_set = ROUTER_SETS.load(storage, router_state.router_set_index)?; + + Ok(RouterVerifierConfig { + router_set_index: router_state.router_set_index, + routers: router_set.routers, + expected_emitter_chain: config.expected_emitter_chain, + expected_emitter_address: config.expected_emitter_address, }) } @@ -108,10 +196,16 @@ fn query_config(deps: Deps) -> StdResult { mod tests { use super::*; use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; - use cosmwasm_std::Binary; + use cosmwasm_std::{to_json_binary, Binary}; + use k256::ecdsa::{RecoveryId, Signature, SigningKey, VerifyingKey}; + use sha3::{Digest, Keccak256}; use crate::msg::{RouterAddress, RouterVerifierConfigMsg}; + const EMITTER_CHAIN: u16 = 26; + const EMITTER_ADDRESS: [u8; 32] = *b"PythnetPythnetPythnetPythnetPyth"; + const GOVERNANCE_TARGET_CHAIN: u16 = 29; + fn router_config() -> RouterVerifierConfigMsg { RouterVerifierConfigMsg { router_set_index: 0, @@ -140,6 +234,7 @@ mod tests { let mut deps = mock_dependencies(); let msg = InstantiateMsg { admin: deps.api.addr_make("admin").to_string(), + governance_target_chain: None, router_verifier: router_config(), }; let sender = deps.api.addr_make("sender"); @@ -150,4 +245,485 @@ mod tests { assert_eq!(response.router_verifier.routers.len(), 5); assert_eq!(response.router_verifier.expected_emitter_chain, 26); } + + #[test] + fn submit_v_a_a_rotates_to_next_signed_router_set() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate( + deps.as_mut(), + mock_env(), + message_info(&sender, &[]), + InstantiateMsg { + admin: admin.to_string(), + governance_target_chain: None, + router_verifier: RouterVerifierConfigMsg { + router_set_index: 0, + routers: current_keys.iter().map(router_address_msg).collect(), + expected_emitter_chain: EMITTER_CHAIN, + expected_emitter_address: Binary::from(EMITTER_ADDRESS), + }, + }, + ) + .unwrap(); + + let msg = ExecuteMsg::SubmitVAA { + vaa: Binary::default(), + }; + let json = to_json_binary(&msg).unwrap(); + assert_eq!( + std::str::from_utf8(json.as_slice()).unwrap(), + r#"{"submit_v_a_a":{"vaa":""}}"# + ); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet(router_set_update_payload(1, &next_keys)), + ); + + execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap(); + + let response = query_config(deps.as_ref()).unwrap(); + assert_eq!(response.router_verifier.router_set_index, 1); + assert_eq!( + response + .router_verifier + .routers + .iter() + .map(|router| router.bytes.to_vec()) + .collect::>(), + next_keys + .iter() + .map(|key| address_from_key(key.verifying_key()).to_vec()) + .collect::>() + ); + } + + #[test] + fn submit_v_a_a_rejects_admin_without_current_router_signatures() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + let outsider_keys = router_keys(20); + + instantiate_with_keys(deps.as_mut(), &sender, &admin, ¤t_keys); + + let router_update_vaa = signed_vaa( + &outsider_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet(router_set_update_payload(1, &next_keys)), + ); + + let err = execute( + deps.as_mut(), + mock_env(), + message_info(&admin, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::RouterSignatureError)); + assert_active_router_set(deps.as_ref(), 0, ¤t_keys); + } + + #[test] + fn submit_v_a_a_rejects_skipped_router_set_index() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate_with_keys(deps.as_mut(), &sender, &admin, ¤t_keys); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet(router_set_update_payload(2, &next_keys)), + ); + + let err = execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::RouterSetIndexIncreaseError)); + assert_active_router_set(deps.as_ref(), 0, ¤t_keys); + } + + #[test] + fn submit_v_a_a_rejects_wrong_emitter() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate_with_keys(deps.as_mut(), &sender, &admin, ¤t_keys); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN + 1, + EMITTER_ADDRESS, + governance_packet(router_set_update_payload(1, &next_keys)), + ); + + let err = execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::InvalidEmitter)); + assert_active_router_set(deps.as_ref(), 0, ¤t_keys); + } + + #[test] + fn submit_v_a_a_accepts_configured_governance_target_chain() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate_with_keys_and_target( + deps.as_mut(), + &sender, + &admin, + ¤t_keys, + Some(GOVERNANCE_TARGET_CHAIN), + ); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet_for_chain( + GOVERNANCE_TARGET_CHAIN, + router_set_update_payload(1, &next_keys), + ), + ); + + execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap(); + + assert_active_router_set(deps.as_ref(), 1, &next_keys); + } + + #[test] + fn submit_v_a_a_rejects_wrong_governance_target_chain() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate_with_keys_and_target( + deps.as_mut(), + &sender, + &admin, + ¤t_keys, + Some(GOVERNANCE_TARGET_CHAIN), + ); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet_for_chain( + GOVERNANCE_TARGET_CHAIN + 1, + router_set_update_payload(1, &next_keys), + ), + ); + + let err = execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::InvalidGovernanceTarget)); + assert_active_router_set(deps.as_ref(), 0, ¤t_keys); + } + + #[test] + fn submit_v_a_a_rejects_replay_after_rotation() { + let mut deps = mock_dependencies(); + let sender = deps.api.addr_make("sender"); + let admin = deps.api.addr_make("admin"); + let submitter = deps.api.addr_make("anyone"); + let current_keys = router_keys(1); + let next_keys = router_keys(6); + + instantiate_with_keys(deps.as_mut(), &sender, &admin, ¤t_keys); + + let router_update_vaa = signed_vaa( + ¤t_keys, + &[0, 1, 2], + 0, + EMITTER_CHAIN, + EMITTER_ADDRESS, + governance_packet(router_set_update_payload(1, &next_keys)), + ); + + execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa.clone()), + }, + ) + .unwrap(); + + let err = execute( + deps.as_mut(), + mock_env(), + message_info(&submitter, &[]), + ExecuteMsg::SubmitVAA { + vaa: Binary::from(router_update_vaa), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::InvalidRouterSetIndex)); + assert_active_router_set(deps.as_ref(), 1, &next_keys); + } + + #[test] + fn migrate_splits_legacy_router_config_into_state_and_sets() { + let mut deps = mock_dependencies(); + let admin = deps.api.addr_make("admin"); + let current_keys = router_keys(1); + let router_verifier = RouterVerifierConfig { + router_set_index: 0, + routers: current_keys + .iter() + .map(|key| address_from_key(key.verifying_key()).to_vec()) + .collect(), + expected_emitter_chain: EMITTER_CHAIN, + expected_emitter_address: EMITTER_ADDRESS.to_vec(), + }; + + LEGACY_CONFIG + .save( + deps.as_mut().storage, + &crate::state::LegacyConfig { + admin: admin.clone(), + router_verifier, + }, + ) + .unwrap(); + + migrate( + deps.as_mut(), + mock_env(), + MigrateMsg { + governance_target_chain: Some(GOVERNANCE_TARGET_CHAIN), + }, + ) + .unwrap(); + + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!(config.admin, admin); + assert_eq!(config.governance_target_chain, GOVERNANCE_TARGET_CHAIN); + assert_eq!(config.expected_emitter_chain, EMITTER_CHAIN); + assert_eq!(config.expected_emitter_address, EMITTER_ADDRESS); + assert_active_router_set(deps.as_ref(), 0, ¤t_keys); + } + + fn instantiate_with_keys( + deps: DepsMut, + sender: &cosmwasm_std::Addr, + admin: &cosmwasm_std::Addr, + keys: &[SigningKey], + ) { + instantiate_with_keys_and_target(deps, sender, admin, keys, None); + } + + fn instantiate_with_keys_and_target( + deps: DepsMut, + sender: &cosmwasm_std::Addr, + admin: &cosmwasm_std::Addr, + keys: &[SigningKey], + governance_target_chain: Option, + ) { + instantiate( + deps, + mock_env(), + message_info(sender, &[]), + InstantiateMsg { + admin: admin.to_string(), + governance_target_chain, + router_verifier: RouterVerifierConfigMsg { + router_set_index: 0, + routers: keys.iter().map(router_address_msg).collect(), + expected_emitter_chain: EMITTER_CHAIN, + expected_emitter_address: Binary::from(EMITTER_ADDRESS), + }, + }, + ) + .unwrap(); + } + + fn assert_active_router_set(deps: Deps, index: u32, keys: &[SigningKey]) { + let response = query_config(deps).unwrap(); + assert_eq!(response.router_verifier.router_set_index, index); + assert_eq!( + response + .router_verifier + .routers + .iter() + .map(|router| router.bytes.to_vec()) + .collect::>(), + keys.iter() + .map(|key| address_from_key(key.verifying_key()).to_vec()) + .collect::>() + ); + } + + fn router_keys(start: u8) -> Vec { + (start..start + 5) + .map(|i| SigningKey::from_bytes((&[i; 32]).into()).unwrap()) + .collect() + } + + fn router_address_msg(key: &SigningKey) -> RouterAddress { + RouterAddress { + bytes: Binary::from(address_from_key(key.verifying_key())), + } + } + + fn address_from_key(key: &VerifyingKey) -> [u8; 20] { + let point = key.to_encoded_point(false); + let hash = Keccak256::digest(&point.as_bytes()[1..]); + hash[12..].try_into().unwrap() + } + + fn router_set_update_payload(router_set_index: u32, keys: &[SigningKey]) -> Vec { + let mut payload = Vec::with_capacity(5 + keys.len() * 20); + payload.extend_from_slice(&router_set_index.to_be_bytes()); + payload.push(keys.len() as u8); + for key in keys { + payload.extend_from_slice(&address_from_key(key.verifying_key())); + } + payload + } + + fn governance_packet(inner_payload: Vec) -> Vec { + governance_packet_for_chain(0, inner_payload) + } + + fn governance_packet_for_chain(target_chain: u16, inner_payload: Vec) -> Vec { + let mut payload = vec![0u8; 32]; + payload[28..].copy_from_slice(b"Core"); + payload.push(2); + payload.extend_from_slice(&target_chain.to_be_bytes()); + payload.extend_from_slice(&inner_payload); + payload + } + + fn signed_vaa( + keys: &[SigningKey], + signer_indexes: &[u8], + router_set_index: u32, + emitter_chain: u16, + emitter_address: [u8; 32], + payload: Vec, + ) -> Vec { + let body = vaa_body(emitter_chain, emitter_address, payload); + let hash = body_hash(&body); + + let mut vaa = vec![1u8]; + vaa.extend_from_slice(&router_set_index.to_be_bytes()); + vaa.push(signer_indexes.len() as u8); + + for index in signer_indexes { + let (signature, recovery_id) = sign_hash(&keys[*index as usize], &hash); + vaa.push(*index); + vaa.extend_from_slice(&signature.to_bytes()); + vaa.push(recovery_id.to_byte()); + } + + vaa.extend_from_slice(&body); + vaa + } + + fn vaa_body(emitter_chain: u16, emitter_address: [u8; 32], payload: Vec) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&123u32.to_be_bytes()); + body.extend_from_slice(&456u32.to_be_bytes()); + body.extend_from_slice(&emitter_chain.to_be_bytes()); + body.extend_from_slice(&emitter_address); + body.extend_from_slice(&789u64.to_be_bytes()); + body.push(0); + body.extend_from_slice(&payload); + body + } + + fn body_hash(body: &[u8]) -> [u8; 32] { + let first = Keccak256::digest(body); + Keccak256::digest(first).into() + } + + fn sign_hash(key: &SigningKey, hash: &[u8; 32]) -> (Signature, RecoveryId) { + key.sign_prehash_recoverable(hash).unwrap() + } } diff --git a/contracts/pyth_vaa/src/error.rs b/contracts/pyth_vaa/src/error.rs index 4ab4afce5..642e5a542 100644 --- a/contracts/pyth_vaa/src/error.rs +++ b/contracts/pyth_vaa/src/error.rs @@ -27,6 +27,9 @@ pub enum ContractError { #[error("InvalidEmitter")] InvalidEmitter, + #[error("InvalidGovernanceTarget")] + InvalidGovernanceTarget, + #[error("NoQuorum")] NoQuorum, @@ -47,4 +50,16 @@ pub enum ContractError { #[error("RouterSignatureError")] RouterSignatureError, + + #[error("InvalidRouterSetUpdate")] + InvalidRouterSetUpdate, + + #[error("InvalidVAAAction")] + InvalidVAAAction, + + #[error("RouterSetIndexIncreaseError")] + RouterSetIndexIncreaseError, + + #[error("RouterSetAlreadyExists")] + RouterSetAlreadyExists, } diff --git a/contracts/pyth_vaa/src/msg.rs b/contracts/pyth_vaa/src/msg.rs index b25226f0d..a9ce12350 100644 --- a/contracts/pyth_vaa/src/msg.rs +++ b/contracts/pyth_vaa/src/msg.rs @@ -4,6 +4,8 @@ use cosmwasm_std::Binary; #[cw_serde] pub struct InstantiateMsg { pub admin: String, + #[serde(default)] + pub governance_target_chain: Option, pub router_verifier: RouterVerifierConfigMsg, } @@ -22,12 +24,14 @@ pub struct RouterAddress { #[cw_serde] pub enum ExecuteMsg { - TransferAdmin { - new_admin: String, - }, - UpdateConfig { - router_verifier: RouterVerifierConfigMsg, - }, + TransferAdmin { new_admin: String }, + SubmitVAA { vaa: Binary }, +} + +#[cw_serde] +pub struct MigrateMsg { + #[serde(default)] + pub governance_target_chain: Option, } #[cw_serde] @@ -43,5 +47,6 @@ pub enum QueryMsg { #[cw_serde] pub struct ConfigResponse { pub admin: String, + pub governance_target_chain: u16, pub router_verifier: RouterVerifierConfigMsg, } diff --git a/contracts/pyth_vaa/src/router.rs b/contracts/pyth_vaa/src/router.rs index a1b525a8a..f0a936ed4 100644 --- a/contracts/pyth_vaa/src/router.rs +++ b/contracts/pyth_vaa/src/router.rs @@ -11,6 +11,19 @@ use crate::{ const ROUTER_COUNT: usize = 5; const ROUTER_QUORUM: usize = 3; +const ROUTER_ADDRESS_LEN: usize = 20; +const GOVERNANCE_PACKET_LEN: usize = 35; +const GOVERNANCE_MODULE_LEN: usize = 32; +const GOVERNANCE_ACTION_POS: usize = 32; +const GOVERNANCE_TARGET_CHAIN_POS: usize = 33; +const GOVERNANCE_PAYLOAD_POS: usize = 35; +const GOVERNANCE_ACTION_ROUTER_SET_UPGRADE: u8 = 2; +const GOVERNANCE_TARGET_CHAIN_GLOBAL: u16 = 0; + +pub struct RouterSetUpdate { + pub router_set_index: u32, + pub routers: Vec>, +} pub fn parse_config(msg: RouterVerifierConfigMsg) -> Result { if msg.routers.len() != ROUTER_COUNT { @@ -19,14 +32,7 @@ pub fn parse_config(msg: RouterVerifierConfigMsg) -> Result> = Vec::with_capacity(ROUTER_COUNT); for router in msg.routers { - let bytes = router.bytes.as_slice(); - if bytes.len() != 20 { - return Err(ContractError::InvalidAddressLength); - } - if routers.iter().any(|existing| existing.as_slice() == bytes) { - return Err(ContractError::InvalidConfig); - } - routers.push(bytes.to_vec()); + push_router_address(&mut routers, router.bytes.as_slice())?; } if msg.expected_emitter_address.len() != 32 { @@ -57,6 +63,74 @@ pub fn config_to_msg(config: &RouterVerifierConfig) -> RouterVerifierConfigMsg { } } +pub fn parse_router_set_update( + data: &[u8], + governance_target_chain: u16, +) -> Result { + let data = parse_governance_router_set_update(data, governance_target_chain)?; + + if data.len() < 5 { + return Err(ContractError::InvalidRouterSetUpdate); + } + + let router_set_index = u32::from_be_bytes( + data[0..4] + .try_into() + .map_err(|_| ContractError::InvalidRouterSetUpdate)?, + ); + let router_count = data[4] as usize; + if router_count != ROUTER_COUNT { + return Err(ContractError::InvalidConfig); + } + + let expected_len = 5 + router_count * ROUTER_ADDRESS_LEN; + if data.len() != expected_len { + return Err(ContractError::InvalidRouterSetUpdate); + } + + let mut routers = Vec::with_capacity(router_count); + for i in 0..router_count { + let pos = 5 + i * ROUTER_ADDRESS_LEN; + push_router_address(&mut routers, &data[pos..pos + ROUTER_ADDRESS_LEN])?; + } + + Ok(RouterSetUpdate { + router_set_index, + routers, + }) +} + +fn parse_governance_router_set_update( + data: &[u8], + governance_target_chain: u16, +) -> Result<&[u8], ContractError> { + if data.len() < GOVERNANCE_PACKET_LEN { + return Err(ContractError::InvalidRouterSetUpdate); + } + + let module = String::from_utf8(data[..GOVERNANCE_MODULE_LEN].to_vec()) + .map_err(|_| ContractError::InvalidVAAAction)?; + let module = module.trim_matches(char::from(0)); + if module != "Core" { + return Err(ContractError::InvalidVAAAction); + } + + if data[GOVERNANCE_ACTION_POS] != GOVERNANCE_ACTION_ROUTER_SET_UPGRADE { + return Err(ContractError::InvalidVAAAction); + } + + let target_chain = u16::from_be_bytes( + data[GOVERNANCE_TARGET_CHAIN_POS..GOVERNANCE_PAYLOAD_POS] + .try_into() + .map_err(|_| ContractError::InvalidRouterSetUpdate)?, + ); + if target_chain != GOVERNANCE_TARGET_CHAIN_GLOBAL && target_chain != governance_target_chain { + return Err(ContractError::InvalidGovernanceTarget); + } + + Ok(&data[GOVERNANCE_PAYLOAD_POS..]) +} + pub fn verify_vaa(config: &RouterVerifierConfig, data: &[u8]) -> Result { let vaa = ParsedVAA::deserialize(data)?; @@ -134,6 +208,17 @@ fn router_address(key: &VerifyingKey) -> Vec { hash[12..].to_vec() } +fn push_router_address(routers: &mut Vec>, bytes: &[u8]) -> Result<(), ContractError> { + if bytes.len() != ROUTER_ADDRESS_LEN { + return Err(ContractError::InvalidAddressLength); + } + if routers.iter().any(|existing| existing.as_slice() == bytes) { + return Err(ContractError::InvalidConfig); + } + routers.push(bytes.to_vec()); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/contracts/pyth_vaa/src/state.rs b/contracts/pyth_vaa/src/state.rs index 2a8d386ab..bbe0abdad 100644 --- a/contracts/pyth_vaa/src/state.rs +++ b/contracts/pyth_vaa/src/state.rs @@ -1,13 +1,31 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; -use cw_storage_plus::Item; +use cw_storage_plus::{Item, Map}; #[cw_serde] pub struct Config { + pub admin: Addr, + pub governance_target_chain: u16, + pub expected_emitter_chain: u16, + pub expected_emitter_address: Vec, +} + +#[cw_serde] +pub struct RouterState { + pub router_set_index: u32, +} + +#[cw_serde] +pub struct LegacyConfig { pub admin: Addr, pub router_verifier: RouterVerifierConfig, } +#[cw_serde] +pub struct RouterSet { + pub routers: Vec>, +} + #[cw_serde] pub struct RouterVerifierConfig { pub router_set_index: u32, @@ -17,3 +35,6 @@ pub struct RouterVerifierConfig { } pub const CONFIG: Item = Item::new("config"); +pub const LEGACY_CONFIG: Item = Item::new("config"); +pub const ROUTER_STATE: Item = Item::new("router_state"); +pub const ROUTER_SETS: Map = Map::new("router_set");