Pyth to Wormhole on Terra (#629)

* More checks in P2W on Terra

Change-Id: Icbe5d75504f947b741cee1c797740b71456964fe

* Auto-deploy P2W on Terra

Change-Id: I202536fd278aca938e3b8b3cb0a4ceeca314158f

* Don't do replay protection on price updates

We already use the sequence number for replay and rollback protection and can save storage this way

Change-Id: I9e655956aab1ed8dd86b9d821ece2f57900f6c78
This commit is contained in:
Hendrik Hofstadt 2021-12-22 12:22:08 +01:00 committed by GitHub
parent 195a61714e
commit a91fe7797d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 28 deletions

View File

@ -17,10 +17,12 @@
| Token Bridge | SOL | B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE | |
| NFT Bridge | SOL | NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA | |
| Migration Contract | SOL | Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK | |
| P2W Emitter | SOL | 8fuAZUxHecYLMC76ZNjYzwRybUiDv9LhkRQsAccEykLr | |
| Test Wallet | Terra | terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v | Mnemonic: `notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius` |
| Example Token | Terra | terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh | Tokens minted to Test Wallet |
| Bridge Core | Terra | terra18eezxhys9jwku67cm4w84xhnzt4xjj77w2qt62 | |
| Token Bridge | Terra | terra1hqrdl6wstt8qzshwc6mrumpjk9338k0l93hqyd | |
| Bridge Core | Terra | terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 | |
| Token Bridge | Terra | terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4 | |
| Pyth Bridge | Terra | terra1wgh6adn8geywx0v78zs9azrqtqdegufuegnwep | |
| Governance Emitter | Universal | 0x0000000000000000000000000000000000000000000000000000000000000004 / 11111111111111111111111111111115 | Emitter Chain: 0x01 |
### Terra

View File

@ -27,6 +27,8 @@ use crate::{
config_read,
price_info,
price_info_read,
sequence,
sequence_read,
ConfigInfo,
UpgradeContract,
},
@ -65,8 +67,10 @@ pub fn instantiate(
gov_address: msg.gov_address.as_slice().to_vec(),
wormhole_contract: msg.wormhole_contract,
pyth_emitter: msg.pyth_emitter.as_slice().to_vec(),
pyth_emitter_chain: msg.pyth_emitter_chain,
};
config(deps.storage).save(&state)?;
sequence(deps.storage).save(&0)?;
Ok(Response::default())
}
@ -101,22 +105,34 @@ fn submit_vaa(
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 {
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
return ContractError::VaaAlreadyExecuted.std_err();
}
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
return handle_governance_payload(deps, env, &data);
}
// IMPORTANT: VAA replay-protection is not implemented in this code-path
// Sequences are used to prevent replay or price rollbacks
let message =
PriceAttestation::deserialize(&data[..]).map_err(|_| ContractError::InvalidVAA.std())?;
if vaa.emitter_address != state.pyth_emitter {
if vaa.emitter_address != state.pyth_emitter || vaa.emitter_chain != state.pyth_emitter_chain {
return ContractError::InvalidVAA.std_err();
}
// Check sequence
let last_sequence = sequence_read(deps.storage).load()?;
if vaa.sequence <= last_sequence && last_sequence != 0 {
return Err(StdError::generic_err(
"price sequences need to be monotonically increasing",
));
}
sequence(deps.storage).save(&vaa.sequence)?;
// Update price
price_info(deps.storage).save(&message.product_id.to_bytes()[..], &data)?;

View File

@ -4,4 +4,4 @@ extern crate lazy_static;
pub mod contract;
pub mod msg;
pub mod state;
pub mod types;
pub mod types;

View File

@ -1,6 +1,4 @@
use cosmwasm_std::{
Binary,
};
use cosmwasm_std::Binary;
use schemars::JsonSchema;
use serde::{
Deserialize,
@ -17,14 +15,13 @@ pub struct InstantiateMsg {
pub wormhole_contract: HumanAddr,
pub pyth_emitter: Binary,
pub pyth_emitter_chain: u16,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
SubmitVaa {
data: Binary,
},
SubmitVaa { data: Binary },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]

View File

@ -25,6 +25,7 @@ type HumanAddr = String;
pub static CONFIG_KEY: &[u8] = b"config";
pub static PRICE_INFO_KEY: &[u8] = b"price_info";
pub static SEQUENCE_KEY: &[u8] = b"sequence";
// Guardian set information
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -35,6 +36,7 @@ pub struct ConfigInfo {
pub wormhole_contract: HumanAddr,
pub pyth_emitter: Vec<u8>,
pub pyth_emitter_chain: u16,
}
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
@ -45,6 +47,14 @@ pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
singleton_read(storage, CONFIG_KEY)
}
pub fn sequence(storage: &mut dyn Storage) -> Singleton<u64> {
singleton(storage, SEQUENCE_KEY)
}
pub fn sequence_read(storage: &dyn Storage) -> ReadonlySingleton<u64> {
singleton_read(storage, SEQUENCE_KEY)
}
pub fn price_info(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
bucket(storage, PRICE_INFO_KEY)
}
@ -53,7 +63,6 @@ pub fn price_info_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
bucket_read(storage, PRICE_INFO_KEY)
}
pub struct UpgradeContract {
pub new_contract: u64,
}

View File

@ -1,9 +1,7 @@
pub mod pyth_extensions;
use std::{
convert::{
TryInto,
},
convert::TryInto,
io::Read,
mem,
};
@ -38,7 +36,9 @@ pub enum PayloadId {
// On-chain data types
#[derive(Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
#[derive(
Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize,
)]
pub struct PriceAttestation {
pub product_id: Pubkey,
pub price_id: Pubkey,
@ -58,7 +58,7 @@ impl PriceAttestation {
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 {
let PriceAttestation {
product_id,
price_id,
price_type,
@ -131,7 +131,7 @@ impl PriceAttestation {
"Invalid magic {:02X?}, expected {:02X?}",
magic_vec, P2W_MAGIC,
)
.into());
.into());
}
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
@ -143,7 +143,7 @@ impl PriceAttestation {
"Unsupported format version {}, expected {}",
version, P2W_FORMAT_VERSION
)
.into());
.into());
}
let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
@ -155,7 +155,7 @@ impl PriceAttestation {
payload_id_vec[0],
PayloadId::PriceAttestation as u8,
)
.into());
.into());
}
let mut product_id_vec = vec![0u8; PUBKEY_LEN];
@ -190,7 +190,8 @@ impl PriceAttestation {
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 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())?;
@ -204,7 +205,6 @@ impl PriceAttestation {
}
};
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] {

View File

@ -1,7 +1,11 @@
//! 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 std::{
convert::TryInto,
io::Read,
mem,
};
use pyth_client::{
CorpAction,
@ -80,7 +84,9 @@ impl From<&CorpAction> for P2WCorpAction {
}
/// 1:1 Copy of pyth_client::Ema with all-pub fields.
#[derive(Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
#[derive(
Clone, Default, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize,
)]
#[repr(C)]
pub struct P2WEma {
pub val: i64,

View File

@ -54,6 +54,7 @@ async function main() {
"cw20_wrapped.wasm": 4000000,
"wormhole.wasm": 5000000,
"token_bridge.wasm": 6000000,
"pyth_bridge.wasm": 5000000,
};
// Deploy all found WASM files and assign Code IDs.
@ -194,6 +195,40 @@ async function main() {
addresses["mock.wasm"] = address;
});
const pythEmitterAddress =
"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
const pythChain = 1;
// Instantiate Pyth over Wormhole
console.log("Instantiating Pyth over Wormhole");
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
wallet.key.accAddress,
codeIds["pyth_bridge.wasm"],
{
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString(
"base64"
),
pyth_emitter_chain: pythChain,
}
),
],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
const address = /"contract_address","value":"([^"]+)/gm.exec(
rs.raw_log
)[1];
addresses["pyth_bridge.wasm"] = address;
});
console.log(addresses);
const registrations = [