Implement pyth2wormhole for Terra

Change-Id: I3c206cf9850818c1fc012a593ad057e07b5dfa3e
This commit is contained in:
Hendrik Hofstadt 2021-11-23 11:24:47 +01:00
parent c261a97a15
commit 0c747199ab
10 changed files with 1356 additions and 140 deletions

772
terra/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge"] members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/pyth-bridge"]
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
@ -11,3 +11,6 @@ codegen-units = 1
panic = 'abort' panic = 'abort'
incremental = false incremental = false
overflow-checks = true overflow-checks = true
[patch.crates-io]
memmap2 = { git = "https://github.com/certusone/wormhole", package = "memmap2" }

View File

@ -3,4 +3,4 @@
docker run --rm -v "$(pwd)":/code \ docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/workspace-optimizer:0.10.7 cosmwasm/workspace-optimizer:0.12.1

View File

@ -0,0 +1,39 @@
[package]
name = "pyth-bridge"
version = "0.1.0"
authors = ["Wormhole Contributors <contact@certus.one>"]
edition = "2018"
description = "Pyth price receiver"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all init/handle/query exports
library = []
[dependencies]
cosmwasm-std = { version = "0.16.0" }
cosmwasm-storage = { version = "0.16.0" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
serde_derive = { version = "1.0.103"}
cw20 = "0.8.0"
cw20-base = { version = "0.8.0", features = ["library"] }
cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
terraswap = "2.4.0"
wormhole = { path = "../wormhole", features = ["library"] }
thiserror = { version = "1.0.20" }
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
sha3 = { version = "0.9.1", default-features = false }
generic-array = { version = "0.14.4" }
hex = "0.4.2"
lazy_static = "1.4.0"
bigint = "4"
pyth-client = {git = "https://github.com/pyth-network/pyth-client-rs", branch = "v2"}
solana-program = "=1.7.0"
[dev-dependencies]
cosmwasm-vm = { version = "0.16.0", default-features = false }
serde_json = "1.0"

View File

@ -0,0 +1,176 @@
use cosmwasm_std::{
entry_point,
to_binary,
Binary,
CosmosMsg,
Deps,
DepsMut,
Env,
MessageInfo,
QueryRequest,
Response,
StdError,
StdResult,
WasmMsg,
WasmQuery,
};
use crate::{
msg::{
ExecuteMsg,
InstantiateMsg,
MigrateMsg,
QueryMsg,
},
state::{
config,
config_read,
price_info,
price_info_read,
ConfigInfo,
UpgradeContract,
},
types::PriceAttestation,
};
use wormhole::{
byte_utils::get_string_from_32,
error::ContractError,
msg::QueryMsg as WormholeQueryMsg,
state::{
vaa_archive_add,
vaa_archive_check,
GovernancePacket,
ParsedVAA,
},
};
// Chain ID of Terra
const CHAIN_ID: u16 = 3;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// Save general wormhole info
let state = ConfigInfo {
gov_chain: msg.gov_chain,
gov_address: msg.gov_address.as_slice().to_vec(),
wormhole_contract: msg.wormhole_contract,
pyth_emitter: msg.pyth_emitter.as_slice().to_vec(),
};
config(deps.storage).save(&state)?;
Ok(Response::default())
}
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
let cfg = config_read(deps.storage).load()?;
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: cfg.wormhole_contract.clone(),
msg: to_binary(&WormholeQueryMsg::VerifyVAA {
vaa: data.clone(),
block_time,
})?,
}))?;
Ok(vaa)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
match msg {
ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data),
}
}
fn submit_vaa(
mut deps: DepsMut,
env: Env,
_info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
let data = vaa.payload;
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
return ContractError::VaaAlreadyExecuted.std_err();
}
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
// check if vaa is from governance
if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
return handle_governance_payload(deps, env, &data);
}
let message =
PriceAttestation::deserialize(&data[..]).map_err(|_| ContractError::InvalidVAA.std())?;
if vaa.emitter_address != state.pyth_emitter {
return ContractError::InvalidVAA.std_err();
}
// Update price
price_info(deps.storage).save(&message.product_id.to_bytes()[..], &data)?;
Ok(Response::new()
.add_attribute("action", "price_update")
.add_attribute("price_feed", message.product_id.to_string()))
}
fn handle_governance_payload(deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
let gov_packet = GovernancePacket::deserialize(&data)?;
let module = get_string_from_32(&gov_packet.module)?;
if module != "PythBridge" {
return Err(StdError::generic_err("this is not a valid module"));
}
if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
return Err(StdError::generic_err(
"the governance VAA is for another chain",
));
}
match gov_packet.action {
2u8 => handle_upgrade_contract(deps, env, &gov_packet.payload),
_ => ContractError::InvalidVAAAction.std_err(),
}
}
fn handle_upgrade_contract(_deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
let UpgradeContract { new_contract } = UpgradeContract::deserialize(&data)?;
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
contract_addr: env.contract.address.to_string(),
new_code_id: new_contract,
msg: to_binary(&MigrateMsg {})?,
}))
.add_attribute("action", "contract_upgrade"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::PriceInfo { product_id } => {
to_binary(&query_price_info(deps, product_id.as_slice())?)
}
}
}
pub fn query_price_info(deps: Deps, address: &[u8]) -> StdResult<PriceAttestation> {
match price_info_read(deps.storage).load(address) {
Ok(data) => PriceAttestation::deserialize(&data[..]).map_err(|_| {
StdError::parse_err("PriceAttestation", "failed to decode price attestation")
}),
Err(_) => ContractError::AssetNotFound.std_err(),
}
}

View File

@ -0,0 +1,7 @@
#[cfg(test)]
extern crate lazy_static;
pub mod contract;
pub mod msg;
pub mod state;
pub mod types;

View File

@ -0,0 +1,38 @@
use cosmwasm_std::{
Binary,
};
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
type HumanAddr = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
// governance contract details
pub gov_chain: u16,
pub gov_address: Binary,
pub wormhole_contract: HumanAddr,
pub pyth_emitter: Binary,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
SubmitVaa {
data: Binary,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
PriceInfo { product_id: Binary },
}

View File

@ -0,0 +1,67 @@
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use cosmwasm_std::{
StdResult,
Storage,
};
use cosmwasm_storage::{
bucket,
bucket_read,
singleton,
singleton_read,
Bucket,
ReadonlyBucket,
ReadonlySingleton,
Singleton,
};
use wormhole::byte_utils::ByteUtils;
type HumanAddr = String;
pub static CONFIG_KEY: &[u8] = b"config";
pub static PRICE_INFO_KEY: &[u8] = b"price_info";
// Guardian set information
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigInfo {
// governance contract details
pub gov_chain: u16,
pub gov_address: Vec<u8>,
pub wormhole_contract: HumanAddr,
pub pyth_emitter: Vec<u8>,
}
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
singleton(storage, CONFIG_KEY)
}
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
singleton_read(storage, CONFIG_KEY)
}
pub fn price_info(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
bucket(storage, PRICE_INFO_KEY)
}
pub fn price_info_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
bucket_read(storage, PRICE_INFO_KEY)
}
pub struct UpgradeContract {
pub new_contract: u64,
}
impl UpgradeContract {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let new_contract = data.get_u64(24);
Ok(UpgradeContract { new_contract })
}
}

View File

@ -0,0 +1,235 @@
pub mod pyth_extensions;
use std::{
convert::{
TryInto,
},
io::Read,
mem,
};
use solana_program::{
clock::UnixTimestamp,
pubkey::Pubkey,
};
use self::pyth_extensions::{
P2WCorpAction,
P2WEma,
P2WPriceStatus,
P2WPriceType,
};
// Constants and values common to every p2w custom-serialized message
/// Precedes every message implementing the p2w serialization format
pub const P2W_MAGIC: &'static [u8] = b"P2WH";
/// Format version used and understood by this codebase
pub const P2W_FORMAT_VERSION: u16 = 1;
pub const PUBKEY_LEN: usize = 32;
/// Decides the format of following bytes
#[repr(u8)]
pub enum PayloadId {
PriceAttestation = 1,
}
// On-chain data types
#[derive(Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
pub struct PriceAttestation {
pub product_id: Pubkey,
pub price_id: Pubkey,
pub price_type: P2WPriceType,
pub price: i64,
pub expo: i32,
pub twap: P2WEma,
pub twac: P2WEma,
pub confidence_interval: u64,
pub status: P2WPriceStatus,
pub corp_act: P2WCorpAction,
pub timestamp: UnixTimestamp,
}
impl PriceAttestation {
/// Serialize this attestation according to the Pyth-over-wormhole serialization format
pub fn serialize(&self) -> Vec<u8> {
// A nifty trick to get us yelled at if we forget to serialize a field
#[deny(warnings)]
let PriceAttestation {
product_id,
price_id,
price_type,
price,
expo,
twap,
twac,
confidence_interval,
status,
corp_act,
timestamp,
} = self;
// magic
let mut buf = P2W_MAGIC.to_vec();
// version
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
// payload_id
buf.push(PayloadId::PriceAttestation as u8);
// product_id
buf.extend_from_slice(&product_id.to_bytes()[..]);
// price_id
buf.extend_from_slice(&price_id.to_bytes()[..]);
// price_type
buf.push(price_type.clone() as u8);
// price
buf.extend_from_slice(&price.to_be_bytes()[..]);
// exponent
buf.extend_from_slice(&expo.to_be_bytes()[..]);
// twap
buf.append(&mut twap.serialize());
// twac
buf.append(&mut twac.serialize());
// confidence_interval
buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]);
// status
buf.push(status.clone() as u8);
// corp_act
buf.push(corp_act.clone() as u8);
// timestamp
buf.extend_from_slice(&timestamp.to_be_bytes()[..]);
buf
}
pub fn deserialize(mut bytes: impl Read) -> Result<Self, Box<dyn std::error::Error>> {
use P2WCorpAction::*;
use P2WPriceStatus::*;
use P2WPriceType::*;
println!("Using {} bytes for magic", P2W_MAGIC.len());
let mut magic_vec = vec![0u8; P2W_MAGIC.len()];
bytes.read_exact(magic_vec.as_mut_slice())?;
if magic_vec.as_slice() != P2W_MAGIC {
return Err(format!(
"Invalid magic {:02X?}, expected {:02X?}",
magic_vec, P2W_MAGIC,
)
.into());
}
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
bytes.read_exact(version_vec.as_mut_slice())?;
let version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
if version != P2W_FORMAT_VERSION {
return Err(format!(
"Unsupported format version {}, expected {}",
version, P2W_FORMAT_VERSION
)
.into());
}
let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
bytes.read_exact(payload_id_vec.as_mut_slice())?;
if PayloadId::PriceAttestation as u8 != payload_id_vec[0] {
return Err(format!(
"Invalid Payload ID {}, expected {}",
payload_id_vec[0],
PayloadId::PriceAttestation as u8,
)
.into());
}
let mut product_id_vec = vec![0u8; PUBKEY_LEN];
bytes.read_exact(product_id_vec.as_mut_slice())?;
let product_id = Pubkey::new(product_id_vec.as_slice());
let mut price_id_vec = vec![0u8; PUBKEY_LEN];
bytes.read_exact(price_id_vec.as_mut_slice())?;
let price_id = Pubkey::new(price_id_vec.as_slice());
let mut price_type_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
bytes.read_exact(price_type_vec.as_mut_slice())?;
let price_type = match price_type_vec[0] {
a if a == Price as u8 => Price,
a if a == P2WPriceType::Unknown as u8 => P2WPriceType::Unknown,
other => {
return Err(format!("Invalid price_type value {}", other).into());
}
};
let mut price_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(price_vec.as_mut_slice())?;
let price = i64::from_be_bytes(price_vec.as_slice().try_into()?);
let mut expo_vec = vec![0u8; mem::size_of::<i32>()];
bytes.read_exact(expo_vec.as_mut_slice())?;
let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?);
let twap = P2WEma::deserialize(&mut bytes)?;
let twac = P2WEma::deserialize(&mut bytes)?;
println!("twac OK");
let mut confidence_interval_vec = vec![0u8; mem::size_of::<u64>()];
bytes.read_exact(confidence_interval_vec.as_mut_slice())?;
let confidence_interval = u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?);
let mut status_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
bytes.read_exact(status_vec.as_mut_slice())?;
let status = match status_vec[0] {
a if a == P2WPriceStatus::Unknown as u8 => P2WPriceStatus::Unknown,
a if a == Trading as u8 => Trading,
a if a == Halted as u8 => Halted,
a if a == Auction as u8 => Auction,
other => {
return Err(format!("Invalid status value {}", other).into());
}
};
let mut corp_act_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
bytes.read_exact(corp_act_vec.as_mut_slice())?;
let corp_act = match corp_act_vec[0] {
a if a == NoCorpAct as u8 => NoCorpAct,
other => {
return Err(format!("Invalid corp_act value {}", other).into());
}
};
let mut timestamp_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
bytes.read_exact(timestamp_vec.as_mut_slice())?;
let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?);
Ok(Self {
product_id,
price_id,
price_type,
price,
expo,
twap,
twac,
confidence_interval,
status,
corp_act,
timestamp,
})
}
}

View File

@ -0,0 +1,155 @@
//! This module contains 1:1 (or close) copies of selected Pyth types
//! with quick and dirty enhancements.
use std::{convert::TryInto, io::Read, mem};
use pyth_client::{
CorpAction,
Ema,
PriceStatus,
PriceType,
};
/// 1:1 Copy of pyth_client::PriceType with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
#[repr(u8)]
pub enum P2WPriceType {
Unknown,
Price,
}
impl From<&PriceType> for P2WPriceType {
fn from(pt: &PriceType) -> Self {
match pt {
PriceType::Unknown => Self::Unknown,
PriceType::Price => Self::Price,
}
}
}
impl Default for P2WPriceType {
fn default() -> Self {
Self::Price
}
}
/// 1:1 Copy of pyth_client::PriceStatus with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
pub enum P2WPriceStatus {
Unknown,
Trading,
Halted,
Auction,
}
impl From<&PriceStatus> for P2WPriceStatus {
fn from(ps: &PriceStatus) -> Self {
match ps {
PriceStatus::Unknown => Self::Unknown,
PriceStatus::Trading => Self::Trading,
PriceStatus::Halted => Self::Halted,
PriceStatus::Auction => Self::Auction,
}
}
}
impl Default for P2WPriceStatus {
fn default() -> Self {
Self::Trading
}
}
/// 1:1 Copy of pyth_client::CorpAction with derived additional traits.
#[derive(Clone, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
pub enum P2WCorpAction {
NoCorpAct,
}
impl Default for P2WCorpAction {
fn default() -> Self {
Self::NoCorpAct
}
}
impl From<&CorpAction> for P2WCorpAction {
fn from(ca: &CorpAction) -> Self {
match ca {
CorpAction::NoCorpAct => P2WCorpAction::NoCorpAct,
}
}
}
/// 1:1 Copy of pyth_client::Ema with all-pub fields.
#[derive(Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
#[repr(C)]
pub struct P2WEma {
pub val: i64,
pub numer: i64,
pub denom: i64,
}
/// CAUTION: This impl may panic and requires an unsafe cast
impl From<&Ema> for P2WEma {
fn from(ema: &Ema) -> Self {
let our_size = mem::size_of::<P2WEma>();
let upstream_size = mem::size_of::<Ema>();
if our_size == upstream_size {
unsafe { std::mem::transmute_copy(ema) }
} else {
dbg!(our_size);
dbg!(upstream_size);
// Because of private upstream fields it's impossible to
// complain about type-level changes at compile-time
panic!("P2WEma sizeof mismatch")
}
}
}
/// CAUTION: This impl may panic and requires an unsafe cast
impl Into<Ema> for &P2WEma {
fn into(self) -> Ema {
let our_size = mem::size_of::<P2WEma>();
let upstream_size = mem::size_of::<Ema>();
if our_size == upstream_size {
unsafe { std::mem::transmute_copy(self) }
} else {
dbg!(our_size);
dbg!(upstream_size);
// Because of private upstream fields it's impossible to
// complain about type-level changes at compile-time
panic!("P2WEma sizeof mismatch")
}
}
}
impl P2WEma {
pub fn serialize(&self) -> Vec<u8> {
let mut v = vec![];
// val
v.extend(&self.val.to_be_bytes()[..]);
// numer
v.extend(&self.numer.to_be_bytes()[..]);
// denom
v.extend(&self.denom.to_be_bytes()[..]);
v
}
pub fn deserialize(mut bytes: impl Read) -> Result<Self, Box<dyn std::error::Error>> {
let mut val_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(val_vec.as_mut_slice())?;
let val = i64::from_be_bytes(val_vec.as_slice().try_into()?);
let mut numer_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(numer_vec.as_mut_slice())?;
let numer = i64::from_be_bytes(numer_vec.as_slice().try_into()?);
let mut denom_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(denom_vec.as_mut_slice())?;
let denom = i64::from_be_bytes(denom_vec.as_slice().try_into()?);
Ok(Self { val, numer, denom })
}
}