ソースを参照

[cosmos] Implement set fee governance instruction (#423)

* cosmos stuff

* it builds

* fix existing tests

* adding tests

* adding tests

* tidy

* cleanup

* fix

* update deploy script

* doh

* gr

* mabye

* mabye

* pr comments

Co-authored-by: Jayant Krishnamurthy <jkrishnamurthy@jumptrading.com>
Jayant Krishnamurthy 2 年 前
コミット
024d73a8a8

+ 1 - 0
cosmwasm/Cargo.lock

@@ -1239,6 +1239,7 @@ name = "pyth-cosmwasm"
 version = "0.1.0"
 dependencies = [
  "bigint",
+ "byteorder",
  "cosmwasm-std",
  "cosmwasm-storage",
  "cosmwasm-vm",

+ 1 - 1
cosmwasm/README.md

@@ -1,6 +1,6 @@
 # Pyth CosmWasm
 
-This directory contains The Pyth contract on CosmWasm and utilities to deploy it in CosmWasm chains.
+This directory contains the Pyth contract for CosmWasm and utilities to deploy it on CosmWasm chains.
 
 ## Deployment
 

+ 1 - 0
cosmwasm/contracts/pyth/Cargo.toml

@@ -30,6 +30,7 @@ lazy_static = "1.4.0"
 bigint = "4"
 p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
 pyth-sdk-cw = "0.2.0"
+byteorder = "1.4.3"
 
 [dev-dependencies]
 cosmwasm-vm = { version = "1.0.0", default-features = false }

+ 278 - 23
cosmwasm/contracts/pyth/src/contract.rs

@@ -1,6 +1,10 @@
 use {
     crate::{
         error::PythContractError,
+        governance::{
+            GovernanceAction::SetFee,
+            GovernanceInstruction,
+        },
         msg::{
             ExecuteMsg,
             InstantiateMsg,
@@ -16,7 +20,6 @@ use {
             ConfigInfo,
             PriceInfo,
             PythDataSource,
-            VALID_TIME_PERIOD,
         },
     },
     cosmwasm_std::{
@@ -31,6 +34,7 @@ use {
         Response,
         StdResult,
         Timestamp,
+        Uint128,
         WasmQuery,
     },
     p2w_sdk::BatchPriceAttestation,
@@ -40,7 +44,11 @@ use {
         PriceStatus,
         ProductIdentifier,
     },
-    std::collections::HashSet,
+    std::{
+        collections::HashSet,
+        convert::TryFrom,
+        time::Duration,
+    },
     wormhole::{
         msg::QueryMsg as WormholeQueryMsg,
         state::ParsedVAA,
@@ -61,12 +69,21 @@ pub fn instantiate(
 ) -> StdResult<Response> {
     // Save general wormhole and pyth info
     let state = ConfigInfo {
-        owner:             info.sender,
-        wormhole_contract: deps.api.addr_validate(msg.wormhole_contract.as_ref())?,
-        data_sources:      HashSet::from([PythDataSource {
+        owner:                      info.sender,
+        wormhole_contract:          deps.api.addr_validate(msg.wormhole_contract.as_ref())?,
+        data_sources:               HashSet::from([PythDataSource {
             emitter:            msg.pyth_emitter,
             pyth_emitter_chain: msg.pyth_emitter_chain,
         }]),
+        chain_id:                   msg.chain_id,
+        governance_source:          PythDataSource {
+            emitter:            msg.governance_emitter,
+            pyth_emitter_chain: msg.governance_emitter_chain,
+        },
+        governance_sequence_number: msg.governance_sequence_number,
+        valid_time_period:          Duration::from_secs(msg.valid_time_period_secs as u64),
+        fee:                        msg.fee,
+        fee_denom:                  msg.fee_denom,
     };
     config(deps.storage).save(&state)?;
 
@@ -88,7 +105,11 @@ pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<Par
 #[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),
+        ExecuteMsg::UpdatePriceFeeds { data } => update_price_feeds(deps, env, info, &data),
+        ExecuteMsg::ExecuteGovernanceInstruction { data } => {
+            execute_governance_instruction(deps, env, info, &data)
+        }
+        // TODO: remove these and invoke via governance
         ExecuteMsg::AddDataSource { data_source } => add_data_source(deps, env, info, data_source),
         ExecuteMsg::RemoveDataSource { data_source } => {
             remove_data_source(deps, env, info, data_source)
@@ -96,7 +117,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
     }
 }
 
-fn submit_vaa(
+fn update_price_feeds(
     mut deps: DepsMut,
     env: Env,
     _info: MessageInfo,
@@ -106,7 +127,7 @@ fn submit_vaa(
 
     let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
 
-    verify_vaa_sender(&state, &vaa)?;
+    verify_vaa_from_data_source(&state, &vaa)?;
 
     let data = &vaa.payload;
     let batch_attestation = BatchPriceAttestation::deserialize(&data[..])
@@ -115,6 +136,71 @@ fn submit_vaa(
     process_batch_attestation(deps, env, &batch_attestation)
 }
 
+fn execute_governance_instruction(
+    mut deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    data: &Binary,
+) -> StdResult<Response> {
+    let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
+
+    execute_governance_instruction_from_vaa(deps, env, info, &vaa)
+}
+
+/// Helper function to improve testability of governance instructions (so we can unit test without wormhole).
+fn execute_governance_instruction_from_vaa(
+    deps: DepsMut,
+    _env: Env,
+    _info: MessageInfo,
+    vaa: &ParsedVAA,
+) -> StdResult<Response> {
+    let state = config_read(deps.storage).load()?;
+
+    // store updates to the config as a result of this action in here.
+    let mut updated_config: ConfigInfo = state.clone();
+    verify_vaa_from_governance_source(&state, vaa)?;
+
+    if vaa.sequence <= state.governance_sequence_number {
+        return Err(PythContractError::OldGovernanceMessage)?;
+    } else {
+        updated_config.governance_sequence_number = vaa.sequence;
+    }
+
+    let data = &vaa.payload;
+    let instruction = GovernanceInstruction::deserialize(&data[..])
+        .map_err(|_| PythContractError::InvalidGovernancePayload)?;
+
+    if instruction.target_chain_id != state.chain_id && instruction.target_chain_id != 0 {
+        return Err(PythContractError::InvalidGovernancePayload)?;
+    }
+
+    let response = match instruction.action {
+        SetFee { val, expo } => {
+            updated_config.fee = Uint128::new(
+                (val as u128)
+                    .checked_mul(
+                        10_u128
+                            .checked_pow(
+                                u32::try_from(expo)
+                                    .map_err(|_| PythContractError::InvalidGovernancePayload)?,
+                            )
+                            .ok_or(PythContractError::InvalidGovernancePayload)?,
+                    )
+                    .ok_or(PythContractError::InvalidGovernancePayload)?,
+            );
+
+            Response::new()
+                .add_attribute("action", "set_fee")
+                .add_attribute("new_fee", format!("{}", updated_config.fee))
+        }
+        _ => Err(PythContractError::InvalidGovernancePayload)?,
+    };
+
+    config(deps.storage).save(&updated_config)?;
+
+    Ok(response)
+}
+
 fn add_data_source(
     deps: DepsMut,
     _env: Env,
@@ -170,9 +256,8 @@ fn remove_data_source(
 }
 
 
-// This checks the emitter to be the pyth emitter in wormhole and it comes from emitter chain
-// (Solana)
-fn verify_vaa_sender(state: &ConfigInfo, vaa: &ParsedVAA) -> StdResult<()> {
+/// Check that `vaa` is from a valid data source (and hence is a legitimate price update message).
+fn verify_vaa_from_data_source(state: &ConfigInfo, vaa: &ParsedVAA) -> StdResult<()> {
     let vaa_data_source = PythDataSource {
         emitter:            vaa.emitter_address.clone().into(),
         pyth_emitter_chain: vaa.emitter_chain,
@@ -183,6 +268,18 @@ fn verify_vaa_sender(state: &ConfigInfo, vaa: &ParsedVAA) -> StdResult<()> {
     Ok(())
 }
 
+/// Check that `vaa` is from a valid governance source (and hence is a legitimate governance instruction).
+fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> StdResult<()> {
+    let vaa_data_source = PythDataSource {
+        emitter:            vaa.emitter_address.clone().into(),
+        pyth_emitter_chain: vaa.emitter_chain,
+    };
+    if state.governance_source != vaa_data_source {
+        return Err(PythContractError::InvalidUpdateEmitter)?;
+    }
+    Ok(())
+}
+
 fn process_batch_attestation(
     mut deps: DepsMut,
     env: Env,
@@ -273,10 +370,14 @@ fn update_price_feed_if_new(
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
         QueryMsg::PriceFeed { id } => to_binary(&query_price_feed(deps, env, id.as_ref())?),
+        // TODO: implement queries for the update fee and valid time period along the following lines:
+        // QueryMsg::GetUpdateFee { data: bytes[] } => ???,
+        // QueryMsg::GetValidTimePeriod => ???
     }
 }
 
 pub fn query_price_feed(deps: Deps, env: Env, address: &[u8]) -> StdResult<PriceFeedResponse> {
+    let config = config_read(deps.storage).load()?;
     match price_info_read(deps.storage).load(address) {
         Ok(mut price_info) => {
             let env_time_sec = env.block.time.seconds();
@@ -295,7 +396,7 @@ pub fn query_price_feed(deps: Deps, env: Env, address: &[u8]) -> StdResult<Price
                 price_pub_time_sec - env_time_sec
             };
 
-            if time_abs_diff > VALID_TIME_PERIOD.as_secs() {
+            if time_abs_diff > config.valid_time_period.as_secs() {
                 price_info.price_feed.status = PriceStatus::Unknown;
             }
 
@@ -311,6 +412,10 @@ pub fn query_price_feed(deps: Deps, env: Env, address: &[u8]) -> StdResult<Price
 mod test {
     use {
         super::*,
+        crate::governance::GovernanceModule::{
+            Executor,
+            Target,
+        },
         cosmwasm_std::{
             testing::{
                 mock_dependencies,
@@ -323,10 +428,22 @@ mod test {
             Addr,
             OwnedDeps,
         },
+        std::time::Duration,
     };
 
+    /// Default valid time period for testing purposes.
+    const VALID_TIME_PERIOD: Duration = Duration::from_secs(3 * 60);
+
     fn setup_test() -> (OwnedDeps<MockStorage, MockApi, MockQuerier>, Env) {
-        (mock_dependencies(), mock_env())
+        let mut dependencies = mock_dependencies();
+        let mut config = config(dependencies.as_mut().storage);
+        config
+            .save(&ConfigInfo {
+                valid_time_period: VALID_TIME_PERIOD,
+                ..create_zero_config_info()
+            })
+            .unwrap();
+        (dependencies, mock_env())
     }
 
     fn create_zero_vaa() -> ParsedVAA {
@@ -347,9 +464,18 @@ mod test {
 
     fn create_zero_config_info() -> ConfigInfo {
         ConfigInfo {
-            owner:             Addr::unchecked(String::default()),
-            wormhole_contract: Addr::unchecked(String::default()),
-            data_sources:      HashSet::default(),
+            owner:                      Addr::unchecked(String::default()),
+            wormhole_contract:          Addr::unchecked(String::default()),
+            data_sources:               HashSet::default(),
+            governance_source:          PythDataSource {
+                emitter:            Binary(vec![]),
+                pyth_emitter_chain: 0,
+            },
+            governance_sequence_number: 0,
+            chain_id:                   0,
+            valid_time_period:          Duration::new(0, 0),
+            fee:                        Uint128::new(0),
+            fee_denom:                  "".into(),
         }
     }
 
@@ -397,7 +523,7 @@ mod test {
         vaa.emitter_address = vec![1u8];
         vaa.emitter_chain = 3;
 
-        assert_eq!(verify_vaa_sender(&config_info, &vaa), Ok(()));
+        assert_eq!(verify_vaa_from_data_source(&config_info, &vaa), Ok(()));
     }
 
     #[test]
@@ -411,7 +537,7 @@ mod test {
         vaa.emitter_address = vec![3u8, 4u8];
         vaa.emitter_chain = 3;
         assert_eq!(
-            verify_vaa_sender(&config_info, &vaa),
+            verify_vaa_from_data_source(&config_info, &vaa),
             Err(PythContractError::InvalidUpdateEmitter.into())
         );
     }
@@ -427,7 +553,7 @@ mod test {
         vaa.emitter_address = vec![1u8];
         vaa.emitter_chain = 2;
         assert_eq!(
-            verify_vaa_sender(&config_info, &vaa),
+            verify_vaa_from_data_source(&config_info, &vaa),
             Err(PythContractError::InvalidUpdateEmitter.into())
         );
     }
@@ -719,7 +845,7 @@ mod test {
 
         // Should result an error because there is no data source
         assert_eq!(
-            verify_vaa_sender(&config_read(&deps.storage).load().unwrap(), &vaa),
+            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
             Err(PythContractError::InvalidUpdateEmitter.into())
         );
 
@@ -730,7 +856,7 @@ mod test {
         assert!(add_data_source(deps.as_mut(), env, mock_info("123", &[]), data_source).is_ok());
 
         assert_eq!(
-            verify_vaa_sender(&config_read(&deps.storage).load().unwrap(), &vaa),
+            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
             Ok(())
         );
     }
@@ -751,7 +877,7 @@ mod test {
         vaa.emitter_chain = 3;
 
         assert_eq!(
-            verify_vaa_sender(&config_read(&deps.storage).load().unwrap(), &vaa),
+            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
             Ok(())
         );
 
@@ -763,8 +889,137 @@ mod test {
 
         // Should result an error because data source should not exist anymore
         assert_eq!(
-            verify_vaa_sender(&config_read(&deps.storage).load().unwrap(), &vaa),
+            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
             Err(PythContractError::InvalidUpdateEmitter.into())
         );
     }
+
+    /// Initialize the contract with `initial_config` then execute `vaa` as a governance instruction
+    /// against it. Returns the response of the governance instruction along with the resulting config.
+    fn apply_governance_vaa(
+        initial_config: &ConfigInfo,
+        vaa: &ParsedVAA,
+    ) -> StdResult<(Response, ConfigInfo)> {
+        let (mut deps, env) = setup_test();
+        config(&mut deps.storage).save(initial_config).unwrap();
+
+        let info = mock_info("123", &[]);
+
+        let result = execute_governance_instruction_from_vaa(deps.as_mut(), env, info, vaa);
+
+        result.and_then(|response| config_read(&deps.storage).load().map(|c| (response, c)))
+    }
+
+    fn governance_test_config() -> ConfigInfo {
+        ConfigInfo {
+            governance_source: PythDataSource {
+                emitter:            Binary(vec![1u8, 2u8]),
+                pyth_emitter_chain: 3,
+            },
+            governance_sequence_number: 4,
+            chain_id: 5,
+            ..create_zero_config_info()
+        }
+    }
+
+    fn governance_vaa(instruction: &GovernanceInstruction) -> ParsedVAA {
+        let mut vaa = create_zero_vaa();
+        vaa.emitter_address = vec![1u8, 2u8];
+        vaa.emitter_chain = 3;
+        vaa.sequence = 7;
+        vaa.payload = instruction.serialize().unwrap();
+
+        vaa
+    }
+
+    #[test]
+    fn test_governance_authorization() {
+        let test_config = governance_test_config();
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: 5,
+            action:          SetFee { val: 6, expo: 0 },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+
+        // First check that a valid VAA is accepted (to ensure that no one accidentally breaks the following test cases).
+        assert!(apply_governance_vaa(&test_config, &test_vaa).is_ok());
+
+        // Wrong emitter address
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.emitter_address = vec![2u8, 3u8];
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // wrong source chain
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.emitter_chain = 4;
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // sequence number too low
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.sequence = 4;
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // wrong magic number
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.payload[0] = 0;
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // wrong target chain
+        let mut instruction_copy = test_instruction.clone();
+        instruction_copy.target_chain_id = 6;
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.payload = instruction_copy.serialize().unwrap();
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // target chain == 0 is allowed
+        let mut instruction_copy = test_instruction.clone();
+        instruction_copy.target_chain_id = 0;
+        let mut vaa_copy = test_vaa.clone();
+        vaa_copy.payload = instruction_copy.serialize().unwrap();
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_ok());
+
+        // wrong module
+        let mut instruction_copy = test_instruction.clone();
+        instruction_copy.module = Executor;
+        let mut vaa_copy = test_vaa;
+        vaa_copy.payload = instruction_copy.serialize().unwrap();
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+
+        // invalid action index
+        let _instruction_copy = test_instruction;
+        vaa_copy.payload[9] = 100;
+        assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
+    }
+
+    #[test]
+    fn test_set_fee() {
+        let mut test_config = governance_test_config();
+        test_config.fee = Uint128::new(1);
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: 5,
+            action:          SetFee { val: 6, expo: 1 },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa).map(|(_r, c)| c.fee),
+            Ok(Uint128::new(60))
+        );
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: 5,
+            action:          SetFee { val: 6, expo: 0 },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa).map(|(_r, c)| c.fee),
+            Ok(Uint128::new(6))
+        );
+    }
 }

+ 12 - 0
cosmwasm/contracts/pyth/src/error.rs

@@ -28,6 +28,18 @@ pub enum PythContractError {
     /// Data source already exists error (on adding data source)
     #[error("DataSourceAlreadyExists")]
     DataSourceAlreadyExists,
+
+    /// Message emitter is not an accepted source of governance instructions.
+    #[error("InvalidGovernanceEmitter")]
+    InvalidGovernanceEmitter,
+
+    /// Message payload cannot be deserialized as a valid governance instruction.
+    #[error("InvalidGovernancePayload")]
+    InvalidGovernancePayload,
+
+    /// The sequence number of the governance message is too old.
+    #[error("OldGovernanceMessage")]
+    OldGovernanceMessage,
 }
 
 impl From<PythContractError> for StdError {

+ 147 - 0
cosmwasm/contracts/pyth/src/governance.rs

@@ -0,0 +1,147 @@
+use {
+    byteorder::{
+        BigEndian,
+        ReadBytesExt,
+        WriteBytesExt,
+    },
+    p2w_sdk::ErrBox,
+    schemars::JsonSchema,
+    serde::{
+        Deserialize,
+        Serialize,
+    },
+    std::io::Write,
+};
+
+const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
+
+/// The type of contract that can accept a governance instruction.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[repr(u8)]
+pub enum GovernanceModule {
+    /// The PythNet executor contract
+    Executor = 0,
+    /// A target chain contract (like this one!)
+    Target   = 1,
+}
+
+impl GovernanceModule {
+    pub fn from_u8(x: u8) -> Result<GovernanceModule, ErrBox> {
+        match x {
+            0 => Ok(GovernanceModule::Executor),
+            1 => Ok(GovernanceModule::Target),
+            _ => Err(format!("Invalid governance module: {x}",).into()),
+        }
+    }
+
+    pub fn to_u8(&self) -> u8 {
+        match &self {
+            GovernanceModule::Executor => 0,
+            GovernanceModule::Target => 1,
+        }
+    }
+}
+
+/// The action to perform to change the state of the target chain contract.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[repr(u8)]
+pub enum GovernanceAction {
+    UpgradeContract,                       // 0
+    AuthorizeGovernanceDataSourceTransfer, // 1
+    SetDataSources,                        // 2
+    // Set the fee to val * (10 ** expo)
+    SetFee { val: u64, expo: u64 }, // 3
+    // Set the default valid period to the provided number of seconds
+    SetValidPeriod { valid_seconds: u64 }, // 4
+    RequestGovernanceDataSourceTransfer,   // 5
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+pub struct GovernanceInstruction {
+    pub module:          GovernanceModule,
+    pub action:          GovernanceAction,
+    pub target_chain_id: u16,
+}
+
+impl GovernanceInstruction {
+    pub fn deserialize(mut bytes: impl ReadBytesExt) -> Result<Self, ErrBox> {
+        let mut magic_vec = vec![0u8; PYTH_GOVERNANCE_MAGIC.len()];
+        bytes.read_exact(magic_vec.as_mut_slice())?;
+
+        if magic_vec.as_slice() != PYTH_GOVERNANCE_MAGIC {
+            return Err(format!(
+                "Invalid magic {magic_vec:02X?}, expected {PYTH_GOVERNANCE_MAGIC:02X?}",
+            )
+            .into());
+        }
+
+        let module_num = bytes.read_u8()?;
+        let module = GovernanceModule::from_u8(module_num)?;
+
+        if module != GovernanceModule::Target {
+            return Err(format!("Invalid governance module {module_num}",).into());
+        }
+
+        let action_type: u8 = bytes.read_u8()?;
+        let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
+
+        let action: Result<GovernanceAction, String> = match action_type {
+            3 => {
+                let val = bytes.read_u64::<BigEndian>()?;
+                let expo = bytes.read_u64::<BigEndian>()?;
+                Ok(GovernanceAction::SetFee { val, expo })
+            }
+            // TODO: add parsing for additional actions
+            _ => Err(format!("Bad governance action {action_type}",)),
+        };
+
+        Ok(GovernanceInstruction {
+            module,
+            action: action?,
+            target_chain_id,
+        })
+    }
+
+    pub fn serialize(&self) -> Result<Vec<u8>, ErrBox> {
+        let mut buf = vec![];
+
+        buf.write_all(PYTH_GOVERNANCE_MAGIC)?;
+        buf.write_u8(self.module.to_u8())?;
+
+        match &self.action {
+            GovernanceAction::UpgradeContract => {
+                buf.write_u8(0)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+            }
+            GovernanceAction::AuthorizeGovernanceDataSourceTransfer => {
+                buf.write_u8(1)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+            }
+            GovernanceAction::SetDataSources => {
+                buf.write_u8(2)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+            }
+            GovernanceAction::SetFee { val, expo } => {
+                buf.write_u8(3)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+
+                buf.write_u64::<BigEndian>(*val)?;
+                buf.write_u64::<BigEndian>(*expo)?;
+            }
+            GovernanceAction::SetValidPeriod {
+                valid_seconds: new_valid_period,
+            } => {
+                buf.write_u8(4)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+
+                buf.write_u64::<BigEndian>(*new_valid_period)?;
+            }
+            GovernanceAction::RequestGovernanceDataSourceTransfer => {
+                buf.write_u8(5)?;
+                buf.write_u16::<BigEndian>(self.target_chain_id)?;
+            }
+        }
+
+        Ok(buf)
+    }
+}

+ 1 - 0
cosmwasm/contracts/pyth/src/lib.rs

@@ -3,5 +3,6 @@ extern crate lazy_static;
 
 pub mod contract;
 pub mod error;
+pub mod governance;
 pub mod msg;
 pub mod state;

+ 19 - 5
cosmwasm/contracts/pyth/src/msg.rs

@@ -1,6 +1,9 @@
 use {
     crate::state::PythDataSource,
-    cosmwasm_std::Binary,
+    cosmwasm_std::{
+        Binary,
+        Uint128,
+    },
     schemars::JsonSchema,
     serde::{
         Deserialize,
@@ -12,17 +15,28 @@ type HumanAddr = String;
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct InstantiateMsg {
-    pub wormhole_contract:  HumanAddr,
-    pub pyth_emitter:       Binary,
-    pub pyth_emitter_chain: u16,
+    pub wormhole_contract:          HumanAddr,
+    // TODO: this should support multiple emitters
+    pub pyth_emitter:               Binary,
+    pub pyth_emitter_chain:         u16,
+    pub governance_emitter:         Binary,
+    pub governance_emitter_chain:   u16,
+    pub governance_sequence_number: u64,
+    pub chain_id:                   u16,
+    pub valid_time_period_secs:     u16,
+
+    pub fee:       Uint128,
+    pub fee_denom: String,
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum ExecuteMsg {
-    SubmitVaa { data: Binary },
+    // TODO: add UpdatePriceFeeds if necessary
+    UpdatePriceFeeds { data: Binary },
     AddDataSource { data_source: PythDataSource },
     RemoveDataSource { data_source: PythDataSource },
+    ExecuteGovernanceInstruction { data: Binary },
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]

+ 14 - 8
cosmwasm/contracts/pyth/src/state.rs

@@ -4,6 +4,7 @@ use {
         Binary,
         Storage,
         Timestamp,
+        Uint128,
     },
     cosmwasm_storage::{
         bucket,
@@ -30,11 +31,6 @@ use {
 pub static CONFIG_KEY: &[u8] = b"config";
 pub static PRICE_INFO_KEY: &[u8] = b"price_info_v3";
 
-/// Maximum acceptable time period before price is considered to be stale.
-///
-/// This value considers attestation delay which currently might up to a minute.
-pub const VALID_TIME_PERIOD: Duration = Duration::from_secs(3 * 60);
-
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
 pub struct PythDataSource {
     pub emitter:            Binary,
@@ -44,9 +40,19 @@ pub struct PythDataSource {
 // Guardian set information
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct ConfigInfo {
-    pub owner:             Addr,
-    pub wormhole_contract: Addr,
-    pub data_sources:      HashSet<PythDataSource>,
+    pub owner:                      Addr,
+    pub wormhole_contract:          Addr,
+    pub data_sources:               HashSet<PythDataSource>,
+    pub governance_source:          PythDataSource,
+    pub governance_sequence_number: u64,
+    // FIXME: This id needs to agree with the wormhole chain id.
+    // We should read this directly from wormhole.
+    pub chain_id:                   u16,
+    pub valid_time_period:          Duration,
+
+    // The fee to pay, denominated in fee_denom (typically, the chain's native token)
+    pub fee:       Uint128,
+    pub fee_denom: String,
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]

+ 12 - 0
cosmwasm/tools/deploy.js

@@ -165,6 +165,8 @@ addresses["wormhole.wasm"] = await instantiate(
 
 const pythEmitterAddress =
   "71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
+const pythGovernanceEmitterAddress =
+  "0000000000000000000000000000000000000000000000000000000000001234";
 const pythChain = 1;
 
 addresses["pyth_cosmwasm.wasm"] = await instantiate(
@@ -173,6 +175,16 @@ addresses["pyth_cosmwasm.wasm"] = await instantiate(
     wormhole_contract: addresses["wormhole.wasm"],
     pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString("base64"),
     pyth_emitter_chain: pythChain,
+    governance_emitter: Buffer.from(
+      pythGovernanceEmitterAddress,
+      "hex"
+    ).toString("base64"),
+    governance_emitter_chain: pythChain,
+    governance_sequence_number: 0,
+    chain_id: 3,
+    valid_time_period_secs: 60,
+    fee: "1",
+    fee_denom: "uluna",
   },
   "pyth"
 );

+ 1 - 1
third_party/pyth/p2w-relay/src/relay/terra.ts

@@ -72,7 +72,7 @@ export class TerraRelay implements Relay {
           wallet.key.accAddress,
           this.contractAddress,
           {
-            submit_vaa: {
+            update_price_feeds: {
               data: Buffer.from(signedVAAs[idx], "hex").toString("base64"),
             },
           }