Skip to content
14 changes: 14 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"contracts/wormhole",
"contracts/pyth",
"contracts/pyth-pro-verifier",
]

[workspace.package]
Expand Down
26 changes: 26 additions & 0 deletions contracts/pyth-pro-verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "pyth-pro-verifier"
version = "0.1.0"
authors = ["Overclock Labs"]
edition = "2021"
description = "Query-only Pyth Pro verifier for Akash Pyth price updates"
license = "Apache-2.0"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = []
library = []

[dependencies]
cosmwasm-schema = "3.0.2"
cosmwasm-std = "3.0.2"
cw-storage-plus = "3.0.1"
k256 = { version = "0.13", default-features = false, features = ["ecdsa"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
sha3 = { version = "0.10", default-features = false }
thiserror = "1.0"

[dev-dependencies]
hex = "0.4"
148 changes: 148 additions & 0 deletions contracts/pyth-pro-verifier/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
use sha3::{Digest, Keccak256};

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;

use crate::{
error::ContractError,
msg::{InstantiateMsg, MigrateMsg, ParsedVAA, QueryMsg},
state::{Config, CONFIG},
vaa::{parse_vaa, HEADER_LEN, SIGNATURE_LEN, SIG_DATA_LEN, SIG_DATA_POS, SIG_RECOVERY_POS},
};

const ROUTER_COUNT: usize = 5;
const ROUTER_QUORUM: usize = 3;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
if msg.routers.len() != ROUTER_COUNT {
return ContractError::InvalidConfig.std_err();
}

let mut routers: Vec<Vec<u8>> = Vec::with_capacity(ROUTER_COUNT);
for router in msg.routers {
let bytes = router.bytes.as_slice();
if bytes.len() != 20 {
return ContractError::InvalidAddressLength.std_err();
}
if routers.iter().any(|existing| existing.as_slice() == bytes) {
return ContractError::InvalidConfig.std_err();
}
routers.push(bytes.to_vec());
}

if msg.expected_emitter_address.len() != 32 {
return ContractError::InvalidAddressLength.std_err();
}
let expected_emitter_address = msg.expected_emitter_address.to_vec();

CONFIG.save(
deps.storage,
&Config {
router_set_index: msg.router_set_index,
routers,
expected_emitter_chain: msg.expected_emitter_chain,
expected_emitter_address,
},
)?;

Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::VerifyVAA { vaa, block_time } => {
to_json_binary(&query_verify_vaa(deps, vaa.as_slice(), block_time)?)
}
}
}

pub fn query_verify_vaa(deps: Deps, data: &[u8], _block_time: u64) -> StdResult<ParsedVAA> {
let config = CONFIG.load(deps.storage)?;
let vaa = parse_vaa(data)?;

if vaa.version != 1 {
return ContractError::InvalidVersion.std_err();
}
if vaa.guardian_set_index != config.router_set_index {
return ContractError::InvalidRouterSetIndex.std_err();
}
if vaa.emitter_chain != config.expected_emitter_chain
|| vaa.emitter_address != config.expected_emitter_address
{
return ContractError::InvalidEmitter.std_err();
}

verify_router_signatures(&config, &vaa, data)?;
Ok(vaa)
}

fn verify_router_signatures(config: &Config, vaa: &ParsedVAA, data: &[u8]) -> StdResult<()> {
let signer_count = vaa.len_signers as usize;
if signer_count < ROUTER_QUORUM {
return ContractError::NoQuorum.std_err();
}
if signer_count > config.routers.len() {
return ContractError::TooManySignatures.std_err();
}

let mut last_index: i16 = -1;
let mut pos = HEADER_LEN;
for _ in 0..signer_count {
if pos + SIGNATURE_LEN > data.len() {
return ContractError::InvalidVAA.std_err();
}

let router_index = data[pos] as i16;
if router_index <= last_index {
return ContractError::WrongRouterIndexOrder.std_err();
}
last_index = router_index;

let router_index = router_index as usize;
if router_index >= config.routers.len() {
return ContractError::TooManySignatures.std_err();
}

let signature =
Signature::try_from(&data[pos + SIG_DATA_POS..pos + SIG_DATA_POS + SIG_DATA_LEN])
.map_err(|_| {
cosmwasm_std::StdError::msg(ContractError::CannotDecodeSignature.to_string())
})?;
let recovery_id = RecoveryId::try_from(data[pos + SIG_RECOVERY_POS]).map_err(|_| {
cosmwasm_std::StdError::msg(ContractError::CannotDecodeSignature.to_string())
})?;
let verify_key =
VerifyingKey::recover_from_prehash(vaa.hash.as_slice(), &signature, recovery_id)
.map_err(|_| {
cosmwasm_std::StdError::msg(ContractError::CannotRecoverKey.to_string())
})?;

if router_address(&verify_key) != config.routers[router_index] {
return ContractError::RouterSignatureError.std_err();
}

pos += SIGNATURE_LEN;
}

Ok(())
}

fn router_address(key: &VerifyingKey) -> Vec<u8> {
let point = key.to_encoded_point(false);
let hash = Keccak256::digest(&point.as_bytes()[1..]);
hash[12..].to_vec()
}
36 changes: 36 additions & 0 deletions contracts/pyth-pro-verifier/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
#[error("InvalidConfig")]
InvalidConfig,
#[error("InvalidAddressLength")]
InvalidAddressLength,
#[error("InvalidVAA")]
InvalidVAA,
#[error("InvalidVersion")]
InvalidVersion,
#[error("InvalidRouterSetIndex")]
InvalidRouterSetIndex,
#[error("InvalidEmitter")]
InvalidEmitter,
#[error("NoQuorum")]
NoQuorum,
#[error("WrongRouterIndexOrder")]
WrongRouterIndexOrder,
#[error("TooManySignatures")]
TooManySignatures,
#[error("CannotDecodeSignature")]
CannotDecodeSignature,
#[error("CannotRecoverKey")]
CannotRecoverKey,
#[error("RouterSignatureError")]
RouterSignatureError,
}

impl ContractError {
pub fn std_err<T>(self) -> Result<T, StdError> {
Err(StdError::msg(self.to_string()))
}
}
8 changes: 8 additions & 0 deletions contracts/pyth-pro-verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod contract;
mod error;
pub mod msg;
mod state;
mod vaa;

#[cfg(test)]
mod testing;
40 changes: 40 additions & 0 deletions contracts/pyth-pro-verifier/src/msg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::Binary;

#[cw_serde]
pub struct InstantiateMsg {
pub router_set_index: u32,
pub routers: Vec<RouterAddress>,
pub expected_emitter_chain: u16,
pub expected_emitter_address: Binary,
}

#[cw_serde]
pub struct RouterAddress {
pub bytes: Binary,
}

#[cw_serde]
pub struct MigrateMsg {}

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(ParsedVAA)]
VerifyVAA { vaa: Binary, block_time: u64 },
}

#[cw_serde]
pub struct ParsedVAA {
pub version: u8,
pub guardian_set_index: u32,
pub timestamp: u32,
pub nonce: u32,
pub len_signers: u8,
pub emitter_chain: u16,
pub emitter_address: Vec<u8>,
pub sequence: u64,
pub consistency_level: u8,
pub payload: Vec<u8>,
pub hash: Vec<u8>,
}
12 changes: 12 additions & 0 deletions contracts/pyth-pro-verifier/src/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use cosmwasm_schema::cw_serde;
use cw_storage_plus::Item;

#[cw_serde]
pub struct Config {
pub router_set_index: u32,
pub routers: Vec<Vec<u8>>,
pub expected_emitter_chain: u16,
pub expected_emitter_address: Vec<u8>,
}

pub const CONFIG: Item<Config> = Item::new("config");
Loading
Loading