Переглянути джерело

[cosmos] Implement the other governance instructions (#442)

* initial commit for governance instructions

* merge

* docs

* update deployment code

Co-authored-by: Jayant Krishnamurthy <jkrishnamurthy@jumptrading.com>
Jayant Krishnamurthy 2 роки тому
батько
коміт
9c79ab8862

+ 274 - 223
cosmwasm/contracts/pyth/src/contract.rs

@@ -2,7 +2,14 @@ use {
     crate::{
         error::PythContractError,
         governance::{
-            GovernanceAction::SetFee,
+            GovernanceAction::{
+                AuthorizeGovernanceDataSourceTransfer,
+                RequestGovernanceDataSourceTransfer,
+                SetDataSources,
+                SetFee,
+                SetValidPeriod,
+                UpgradeContract,
+            },
             GovernanceInstruction,
         },
         msg::{
@@ -52,6 +59,7 @@ use {
     std::{
         collections::HashSet,
         convert::TryFrom,
+        iter::FromIterator,
         time::Duration,
     },
     wormhole::{
@@ -85,6 +93,7 @@ pub fn instantiate(
             emitter:            msg.governance_emitter,
             pyth_emitter_chain: msg.governance_emitter_chain,
         },
+        governance_source_index:    msg.governance_source_index,
         governance_sequence_number: msg.governance_sequence_number,
         valid_time_period:          Duration::from_secs(msg.valid_time_period_secs as u64),
         fee:                        msg.fee,
@@ -114,11 +123,6 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
         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)
-        }
     }
 }
 
@@ -173,6 +177,63 @@ fn execute_governance_instruction(
     }
 
     let response = match instruction.action {
+        UpgradeContract { .. } => {
+            // FIXME: implement this
+            Err(PythContractError::InvalidGovernancePayload)?
+        }
+        AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
+            let parsed_claim_vaa = parse_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
+            let claim_vaa_instruction =
+                GovernanceInstruction::deserialize(parsed_claim_vaa.payload.as_slice())
+                    .map_err(|_| PythContractError::InvalidGovernancePayload)?;
+
+            if claim_vaa_instruction.target_chain_id != state.chain_id
+                && claim_vaa_instruction.target_chain_id != 0
+            {
+                Err(PythContractError::InvalidGovernancePayload)?
+            }
+
+            match claim_vaa_instruction.action {
+                RequestGovernanceDataSourceTransfer {
+                    governance_data_source_index,
+                } => {
+                    if state.governance_source_index >= governance_data_source_index {
+                        Err(PythContractError::OldGovernanceMessage)?
+                    }
+
+                    updated_config.governance_source_index = governance_data_source_index;
+                    let new_governance_source = PythDataSource {
+                        emitter:            Binary::from(parsed_claim_vaa.emitter_address.clone()),
+                        pyth_emitter_chain: parsed_claim_vaa.emitter_chain,
+                    };
+                    updated_config.governance_source = new_governance_source;
+                    updated_config.governance_sequence_number = parsed_claim_vaa.sequence;
+
+                    Response::new()
+                        .add_attribute("action", "authorize_governance_data_source_transfer")
+                        .add_attribute(
+                            "new_governance_emitter_address",
+                            format!("{:?}", parsed_claim_vaa.emitter_address),
+                        )
+                        .add_attribute(
+                            "new_governance_emitter_chain",
+                            format!("{}", parsed_claim_vaa.emitter_chain),
+                        )
+                        .add_attribute(
+                            "new_governance_sequence_number",
+                            format!("{}", parsed_claim_vaa.sequence),
+                        )
+                }
+                _ => Err(PythContractError::InvalidGovernancePayload)?,
+            }
+        }
+        SetDataSources { data_sources } => {
+            updated_config.data_sources = HashSet::from_iter(data_sources.iter().cloned());
+
+            Response::new()
+                .add_attribute("action", "set_data_sources")
+                .add_attribute("new_data_sources", format!("{data_sources:?}"))
+        }
         SetFee { val, expo } => {
             updated_config.fee = Uint128::new(
                 (val as u128)
@@ -191,7 +252,18 @@ fn execute_governance_instruction(
                 .add_attribute("action", "set_fee")
                 .add_attribute("new_fee", format!("{}", updated_config.fee))
         }
-        _ => Err(PythContractError::InvalidGovernancePayload)?,
+        SetValidPeriod { valid_seconds } => {
+            updated_config.valid_time_period = Duration::from_secs(valid_seconds);
+
+            Response::new()
+                .add_attribute("action", "set_valid_period")
+                .add_attribute("new_valid_seconds", format!("{valid_seconds}"))
+        }
+        RequestGovernanceDataSourceTransfer { .. } => {
+            // RequestGovernanceDataSourceTransfer can only be part of the
+            // AuthorizeGovernanceDataSourceTransfer message.
+            Err(PythContractError::InvalidGovernancePayload)?
+        }
     };
 
     config(deps.storage).save(&updated_config)?;
@@ -199,61 +271,6 @@ fn execute_governance_instruction(
     Ok(response)
 }
 
-fn add_data_source(
-    deps: DepsMut,
-    _env: Env,
-    info: MessageInfo,
-    data_source: PythDataSource,
-) -> StdResult<Response> {
-    let mut state = config_read(deps.storage).load()?;
-
-    if state.owner != info.sender {
-        return Err(PythContractError::PermissionDenied)?;
-    }
-
-    if !state.data_sources.insert(data_source.clone()) {
-        return Err(PythContractError::DataSourceAlreadyExists)?;
-    }
-
-    config(deps.storage).save(&state)?;
-
-    Ok(Response::new()
-        .add_attribute("action", "add_data_source")
-        .add_attribute("data_source_emitter", format!("{}", data_source.emitter))
-        .add_attribute(
-            "data_source_emitter_chain",
-            format!("{}", data_source.pyth_emitter_chain),
-        ))
-}
-
-fn remove_data_source(
-    deps: DepsMut,
-    _env: Env,
-    info: MessageInfo,
-    data_source: PythDataSource,
-) -> StdResult<Response> {
-    let mut state = config_read(deps.storage).load()?;
-
-    if state.owner != info.sender {
-        return Err(PythContractError::PermissionDenied)?;
-    }
-
-    if !state.data_sources.remove(&data_source) {
-        return Err(PythContractError::DataSourceDoesNotExists)?;
-    }
-
-    config(deps.storage).save(&state)?;
-
-    Ok(Response::new()
-        .add_attribute("action", "remove_data_source")
-        .add_attribute("data_source_emitter", format!("{}", data_source.emitter))
-        .add_attribute(
-            "data_source_emitter_chain",
-            format!("{}", data_source.pyth_emitter_chain),
-        ))
-}
-
-
 /// 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 {
@@ -565,6 +582,7 @@ mod test {
                 emitter:            Binary(vec![]),
                 pyth_emitter_chain: 0,
             },
+            governance_source_index:    0,
             governance_sequence_number: 0,
             chain_id:                   0,
             valid_time_period:          Duration::new(0, 0),
@@ -927,167 +945,6 @@ mod test {
         );
     }
 
-    #[test]
-    fn test_add_data_source_ok_with_owner() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 1,
-        };
-
-        assert!(add_data_source(
-            deps.as_mut(),
-            env.clone(),
-            mock_info("123", &[]),
-            data_source.clone()
-        )
-        .is_ok());
-
-        // Adding an existing data source should result an error
-        assert!(add_data_source(deps.as_mut(), env, mock_info("123", &[]), data_source).is_err());
-    }
-
-    #[test]
-    fn test_add_data_source_err_without_owner() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 1,
-        };
-
-        assert!(add_data_source(deps.as_mut(), env, mock_info("321", &[]), data_source).is_err());
-    }
-
-    #[test]
-    fn test_remove_data_source_ok_with_owner() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                data_sources: create_data_sources(vec![1u8], 1),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 1,
-        };
-
-        assert!(remove_data_source(
-            deps.as_mut(),
-            env.clone(),
-            mock_info("123", &[]),
-            data_source.clone()
-        )
-        .is_ok());
-
-        // Removing a non existent data source should result an error
-        assert!(
-            remove_data_source(deps.as_mut(), env, mock_info("123", &[]), data_source).is_err()
-        );
-    }
-
-    #[test]
-    fn test_remove_data_source_err_without_owner() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                data_sources: create_data_sources(vec![1u8], 1),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 1,
-        };
-
-        assert!(
-            remove_data_source(deps.as_mut(), env, mock_info("321", &[]), data_source).is_err()
-        );
-    }
-
-    #[test]
-    fn test_verify_vaa_works_after_adding_data_source() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let mut vaa = create_zero_vaa();
-        vaa.emitter_address = vec![1u8];
-        vaa.emitter_chain = 3;
-
-        // Should result an error because there is no data source
-        assert_eq!(
-            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
-            Err(PythContractError::InvalidUpdateEmitter.into())
-        );
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 3,
-        };
-        assert!(add_data_source(deps.as_mut(), env, mock_info("123", &[]), data_source).is_ok());
-
-        assert_eq!(
-            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
-            Ok(())
-        );
-    }
-
-    #[test]
-    fn test_verify_vaa_err_after_removing_data_source() {
-        let (mut deps, env) = setup_test();
-        config(&mut deps.storage)
-            .save(&ConfigInfo {
-                owner: Addr::unchecked("123"),
-                data_sources: create_data_sources(vec![1u8], 3),
-                ..create_zero_config_info()
-            })
-            .unwrap();
-
-        let mut vaa = create_zero_vaa();
-        vaa.emitter_address = vec![1u8];
-        vaa.emitter_chain = 3;
-
-        assert_eq!(
-            verify_vaa_from_data_source(&config_read(&deps.storage).load().unwrap(), &vaa),
-            Ok(())
-        );
-
-        let data_source = PythDataSource {
-            emitter:            vec![1u8].into(),
-            pyth_emitter_chain: 3,
-        };
-        assert!(remove_data_source(deps.as_mut(), env, mock_info("123", &[]), data_source).is_ok());
-
-        // Should result an error because data source should not exist anymore
-        assert_eq!(
-            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(
@@ -1188,6 +1045,166 @@ mod test {
         assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
     }
 
+    #[test]
+    fn test_authorize_governance_transfer_success() {
+        let source_2 = PythDataSource {
+            emitter:            Binary::from([2u8; 32]),
+            pyth_emitter_chain: 4,
+        };
+
+        let test_config = governance_test_config();
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          AuthorizeGovernanceDataSourceTransfer {
+                claim_vaa: to_binary(&ParsedVAA {
+                    emitter_address: source_2.emitter.to_vec(),
+                    emitter_chain: source_2.pyth_emitter_chain,
+                    sequence: 12,
+                    payload: GovernanceInstruction {
+                        module:          Target,
+                        target_chain_id: test_config.chain_id,
+                        action:          RequestGovernanceDataSourceTransfer {
+                            governance_data_source_index: 11,
+                        },
+                    }
+                    .serialize()
+                    .unwrap(),
+                    ..create_zero_vaa()
+                })
+                .unwrap(),
+            },
+        };
+
+        let test_vaa = governance_vaa(&test_instruction);
+        let (_response, result_config) = apply_governance_vaa(&test_config, &test_vaa).unwrap();
+        assert_eq!(result_config.governance_source, source_2);
+        assert_eq!(result_config.governance_source_index, 11);
+        assert_eq!(result_config.governance_sequence_number, 12);
+    }
+
+    #[test]
+    fn test_authorize_governance_transfer_bad_source_index() {
+        let source_2 = PythDataSource {
+            emitter:            Binary::from([2u8; 32]),
+            pyth_emitter_chain: 4,
+        };
+
+        let mut test_config = governance_test_config();
+        test_config.governance_source_index = 10;
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          AuthorizeGovernanceDataSourceTransfer {
+                claim_vaa: to_binary(&ParsedVAA {
+                    emitter_address: source_2.emitter.to_vec(),
+                    emitter_chain: source_2.pyth_emitter_chain,
+                    sequence: 12,
+                    payload: GovernanceInstruction {
+                        module:          Target,
+                        target_chain_id: test_config.chain_id,
+                        action:          RequestGovernanceDataSourceTransfer {
+                            governance_data_source_index: 10,
+                        },
+                    }
+                    .serialize()
+                    .unwrap(),
+                    ..create_zero_vaa()
+                })
+                .unwrap(),
+            },
+        };
+
+        let test_vaa = governance_vaa(&test_instruction);
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa),
+            Err(PythContractError::OldGovernanceMessage.into())
+        );
+    }
+
+    #[test]
+    fn test_authorize_governance_transfer_bad_target_chain() {
+        let source_2 = PythDataSource {
+            emitter:            Binary::from([2u8; 32]),
+            pyth_emitter_chain: 4,
+        };
+
+        let test_config = governance_test_config();
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          AuthorizeGovernanceDataSourceTransfer {
+                claim_vaa: to_binary(&ParsedVAA {
+                    emitter_address: source_2.emitter.to_vec(),
+                    emitter_chain: source_2.pyth_emitter_chain,
+                    sequence: 12,
+                    payload: GovernanceInstruction {
+                        module:          Target,
+                        target_chain_id: test_config.chain_id + 1,
+                        action:          RequestGovernanceDataSourceTransfer {
+                            governance_data_source_index: 11,
+                        },
+                    }
+                    .serialize()
+                    .unwrap(),
+                    ..create_zero_vaa()
+                })
+                .unwrap(),
+            },
+        };
+
+        let test_vaa = governance_vaa(&test_instruction);
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa),
+            Err(PythContractError::InvalidGovernancePayload.into())
+        );
+    }
+
+    #[test]
+    fn test_set_data_sources() {
+        let source_1 = PythDataSource {
+            emitter:            Binary::from([1u8; 32]),
+            pyth_emitter_chain: 2,
+        };
+        let source_2 = PythDataSource {
+            emitter:            Binary::from([2u8; 32]),
+            pyth_emitter_chain: 4,
+        };
+        let source_3 = PythDataSource {
+            emitter:            Binary::from([3u8; 32]),
+            pyth_emitter_chain: 6,
+        };
+
+        let mut test_config = governance_test_config();
+        test_config.data_sources = HashSet::from([source_1]);
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          SetDataSources {
+                data_sources: vec![source_2.clone(), source_3.clone()],
+            },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa).map(|(_r, c)| c.data_sources),
+            Ok([source_2, source_3].iter().cloned().collect())
+        );
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          SetDataSources {
+                data_sources: vec![],
+            },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa).map(|(_r, c)| c.data_sources),
+            Ok(HashSet::new())
+        );
+    }
+
     #[test]
     fn test_set_fee() {
         let mut test_config = governance_test_config();
@@ -1217,4 +1234,38 @@ mod test {
             Ok(Uint128::new(6))
         );
     }
+
+    #[test]
+    fn test_set_valid_period() {
+        let mut test_config = governance_test_config();
+        test_config.valid_time_period = Duration::from_secs(10);
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: 5,
+            action:          SetValidPeriod { valid_seconds: 20 },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+
+        assert_eq!(
+            apply_governance_vaa(&test_config, &test_vaa).map(|(_r, c)| c.valid_time_period),
+            Ok(Duration::from_secs(20))
+        );
+    }
+
+    #[test]
+    fn test_request_governance_transfer() {
+        let test_config = governance_test_config();
+
+        let test_instruction = GovernanceInstruction {
+            module:          Target,
+            target_chain_id: test_config.chain_id,
+            action:          RequestGovernanceDataSourceTransfer {
+                governance_data_source_index: 7,
+            },
+        };
+        let test_vaa = governance_vaa(&test_instruction);
+
+        assert!(apply_governance_vaa(&test_config, &test_vaa).is_err());
+    }
 }

+ 77 - 11
cosmwasm/contracts/pyth/src/governance.rs

@@ -1,16 +1,27 @@
 use {
+    crate::{
+        governance::GovernanceAction::{
+            RequestGovernanceDataSourceTransfer,
+            SetValidPeriod,
+        },
+        state::PythDataSource,
+    },
     byteorder::{
         BigEndian,
         ReadBytesExt,
         WriteBytesExt,
     },
+    cosmwasm_std::Binary,
     p2w_sdk::ErrBox,
     schemars::JsonSchema,
     serde::{
         Deserialize,
         Serialize,
     },
-    std::io::Write,
+    std::{
+        convert::TryFrom,
+        io::Write,
+    },
 };
 
 const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
@@ -46,14 +57,14 @@ impl GovernanceModule {
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[repr(u8)]
 pub enum GovernanceAction {
-    UpgradeContract,                       // 0
-    AuthorizeGovernanceDataSourceTransfer, // 1
-    SetDataSources,                        // 2
+    UpgradeContract { address: [u8; 20] }, // 0
+    AuthorizeGovernanceDataSourceTransfer { claim_vaa: Binary }, // 1
+    SetDataSources { data_sources: Vec<PythDataSource> }, // 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
+    RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 }, // 5
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -86,13 +97,50 @@ impl GovernanceInstruction {
         let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
 
         let action: Result<GovernanceAction, String> = match action_type {
+            0 => {
+                let mut address: [u8; 20] = [0; 20];
+                bytes.read_exact(&mut address)?;
+                Ok(GovernanceAction::UpgradeContract { address })
+            }
+            1 => {
+                let mut payload: Vec<u8> = vec![];
+                bytes.read_to_end(&mut payload)?;
+                Ok(GovernanceAction::AuthorizeGovernanceDataSourceTransfer {
+                    claim_vaa: Binary::from(payload),
+                })
+            }
+            2 => {
+                let num_data_sources = bytes.read_u8()?;
+                let mut data_sources: Vec<PythDataSource> = vec![];
+                for _ in 0..num_data_sources {
+                    let chain_id = bytes.read_u16::<BigEndian>()?;
+                    let mut emitter_address: [u8; 32] = [0; 32];
+                    bytes.read_exact(&mut emitter_address)?;
+
+                    data_sources.push(PythDataSource {
+                        emitter:            Binary::from(&emitter_address),
+                        pyth_emitter_chain: chain_id,
+                    });
+                }
+
+                Ok(GovernanceAction::SetDataSources { data_sources })
+            }
             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}",)),
+            4 => {
+                let valid_seconds = bytes.read_u64::<BigEndian>()?;
+                Ok(SetValidPeriod { valid_seconds })
+            }
+            5 => {
+                let governance_data_source_index = bytes.read_u32::<BigEndian>()?;
+                Ok(RequestGovernanceDataSourceTransfer {
+                    governance_data_source_index,
+                })
+            }
+            _ => Err(format!("Unknown governance action type: {action_type}",)),
         };
 
         Ok(GovernanceInstruction {
@@ -109,17 +157,32 @@ impl GovernanceInstruction {
         buf.write_u8(self.module.to_u8())?;
 
         match &self.action {
-            GovernanceAction::UpgradeContract => {
+            GovernanceAction::UpgradeContract { address } => {
                 buf.write_u8(0)?;
                 buf.write_u16::<BigEndian>(self.target_chain_id)?;
+                buf.write_all(address)?;
             }
-            GovernanceAction::AuthorizeGovernanceDataSourceTransfer => {
+            GovernanceAction::AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
                 buf.write_u8(1)?;
                 buf.write_u16::<BigEndian>(self.target_chain_id)?;
+                buf.write_all(claim_vaa.as_slice())?;
             }
-            GovernanceAction::SetDataSources => {
+            GovernanceAction::SetDataSources { data_sources } => {
                 buf.write_u8(2)?;
                 buf.write_u16::<BigEndian>(self.target_chain_id)?;
+                buf.write_u8(u8::try_from(data_sources.len())?)?;
+                for data_source in data_sources {
+                    buf.write_u16::<BigEndian>(data_source.pyth_emitter_chain)?;
+
+                    // The message format expects emitter addresses to be 32 bytes.
+                    // However, we don't maintain this invariant in the rust code (and we violate it in the tests).
+                    // This check gives you a reasonable error message if you happen to violate it in the tests.
+                    if data_source.emitter.len() != 32 {
+                        Err("Emitter addresses must be 32 bytes")?
+                    }
+
+                    buf.write_all(data_source.emitter.as_slice())?;
+                }
             }
             GovernanceAction::SetFee { val, expo } => {
                 buf.write_u8(3)?;
@@ -136,9 +199,12 @@ impl GovernanceInstruction {
 
                 buf.write_u64::<BigEndian>(*new_valid_period)?;
             }
-            GovernanceAction::RequestGovernanceDataSourceTransfer => {
+            GovernanceAction::RequestGovernanceDataSourceTransfer {
+                governance_data_source_index,
+            } => {
                 buf.write_u8(5)?;
                 buf.write_u16::<BigEndian>(self.target_chain_id)?;
+                buf.write_u32::<BigEndian>(*governance_data_source_index)?;
             }
         }
 

+ 1 - 3
cosmwasm/contracts/pyth/src/msg.rs

@@ -1,5 +1,4 @@
 use {
-    crate::state::PythDataSource,
     cosmwasm_std::{
         Binary,
         Uint128,
@@ -26,6 +25,7 @@ pub struct InstantiateMsg {
     pub pyth_emitter_chain:         u16,
     pub governance_emitter:         Binary,
     pub governance_emitter_chain:   u16,
+    pub governance_source_index:    u32,
     pub governance_sequence_number: u64,
     pub chain_id:                   u16,
     pub valid_time_period_secs:     u16,
@@ -39,8 +39,6 @@ pub struct InstantiateMsg {
 pub enum ExecuteMsg {
     // TODO: add UpdatePriceFeeds if necessary
     UpdatePriceFeeds { data: Binary },
-    AddDataSource { data_source: PythDataSource },
-    RemoveDataSource { data_source: PythDataSource },
     ExecuteGovernanceInstruction { data: Binary },
 }
 

+ 10 - 1
cosmwasm/contracts/pyth/src/state.rs

@@ -37,13 +37,22 @@ pub struct PythDataSource {
     pub pyth_emitter_chain: u16,
 }
 
-// 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 governance_source:          PythDataSource,
+    // Index for preventing replay attacks on governance data source transfers.
+    // This index increases every time the governance data source is changed, which prevents old
+    // transfer request VAAs from being replayed.
+    pub governance_source_index:    u32,
+    // The wormhole sequence number for governance messages. This value is increased every time the
+    // a governance instruction is executed.
+    //
+    // This field differs from the one above in that it is generated by wormhole and applicable to all
+    // governance messages, whereas the one above is generated by Pyth and only applicable to governance
+    // source transfers.
     pub governance_sequence_number: u64,
     // FIXME: This id needs to agree with the wormhole chain id.
     // We should read this directly from wormhole.

+ 1 - 0
cosmwasm/tools/deploy.js

@@ -180,6 +180,7 @@ addresses["pyth_cosmwasm.wasm"] = await instantiate(
       "hex"
     ).toString("base64"),
     governance_emitter_chain: pythChain,
+    governance_source_index: 0,
     governance_sequence_number: 0,
     chain_id: 3,
     valid_time_period_secs: 60,