[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>
This commit is contained in:
parent
9ac61742c3
commit
9c79ab8862
|
@ -2,7 +2,14 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
error::PythContractError,
|
error::PythContractError,
|
||||||
governance::{
|
governance::{
|
||||||
GovernanceAction::SetFee,
|
GovernanceAction::{
|
||||||
|
AuthorizeGovernanceDataSourceTransfer,
|
||||||
|
RequestGovernanceDataSourceTransfer,
|
||||||
|
SetDataSources,
|
||||||
|
SetFee,
|
||||||
|
SetValidPeriod,
|
||||||
|
UpgradeContract,
|
||||||
|
},
|
||||||
GovernanceInstruction,
|
GovernanceInstruction,
|
||||||
},
|
},
|
||||||
msg::{
|
msg::{
|
||||||
|
@ -52,6 +59,7 @@ use {
|
||||||
std::{
|
std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
|
iter::FromIterator,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
},
|
},
|
||||||
wormhole::{
|
wormhole::{
|
||||||
|
@ -85,6 +93,7 @@ pub fn instantiate(
|
||||||
emitter: msg.governance_emitter,
|
emitter: msg.governance_emitter,
|
||||||
pyth_emitter_chain: msg.governance_emitter_chain,
|
pyth_emitter_chain: msg.governance_emitter_chain,
|
||||||
},
|
},
|
||||||
|
governance_source_index: msg.governance_source_index,
|
||||||
governance_sequence_number: msg.governance_sequence_number,
|
governance_sequence_number: msg.governance_sequence_number,
|
||||||
valid_time_period: Duration::from_secs(msg.valid_time_period_secs as u64),
|
valid_time_period: Duration::from_secs(msg.valid_time_period_secs as u64),
|
||||||
fee: msg.fee,
|
fee: msg.fee,
|
||||||
|
@ -114,11 +123,6 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
|
||||||
ExecuteMsg::ExecuteGovernanceInstruction { data } => {
|
ExecuteMsg::ExecuteGovernanceInstruction { data } => {
|
||||||
execute_governance_instruction(deps, env, info, &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 {
|
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 } => {
|
SetFee { val, expo } => {
|
||||||
updated_config.fee = Uint128::new(
|
updated_config.fee = Uint128::new(
|
||||||
(val as u128)
|
(val as u128)
|
||||||
|
@ -191,7 +252,18 @@ fn execute_governance_instruction(
|
||||||
.add_attribute("action", "set_fee")
|
.add_attribute("action", "set_fee")
|
||||||
.add_attribute("new_fee", format!("{}", updated_config.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)?;
|
config(deps.storage).save(&updated_config)?;
|
||||||
|
@ -199,61 +271,6 @@ fn execute_governance_instruction(
|
||||||
Ok(response)
|
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).
|
/// 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<()> {
|
fn verify_vaa_from_data_source(state: &ConfigInfo, vaa: &ParsedVAA) -> StdResult<()> {
|
||||||
let vaa_data_source = PythDataSource {
|
let vaa_data_source = PythDataSource {
|
||||||
|
@ -565,6 +582,7 @@ mod test {
|
||||||
emitter: Binary(vec![]),
|
emitter: Binary(vec![]),
|
||||||
pyth_emitter_chain: 0,
|
pyth_emitter_chain: 0,
|
||||||
},
|
},
|
||||||
|
governance_source_index: 0,
|
||||||
governance_sequence_number: 0,
|
governance_sequence_number: 0,
|
||||||
chain_id: 0,
|
chain_id: 0,
|
||||||
valid_time_period: Duration::new(0, 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
|
/// 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.
|
/// against it. Returns the response of the governance instruction along with the resulting config.
|
||||||
fn apply_governance_vaa(
|
fn apply_governance_vaa(
|
||||||
|
@ -1188,6 +1045,166 @@ mod test {
|
||||||
assert!(apply_governance_vaa(&test_config, &vaa_copy).is_err());
|
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]
|
#[test]
|
||||||
fn test_set_fee() {
|
fn test_set_fee() {
|
||||||
let mut test_config = governance_test_config();
|
let mut test_config = governance_test_config();
|
||||||
|
@ -1217,4 +1234,38 @@ mod test {
|
||||||
Ok(Uint128::new(6))
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
use {
|
use {
|
||||||
|
crate::{
|
||||||
|
governance::GovernanceAction::{
|
||||||
|
RequestGovernanceDataSourceTransfer,
|
||||||
|
SetValidPeriod,
|
||||||
|
},
|
||||||
|
state::PythDataSource,
|
||||||
|
},
|
||||||
byteorder::{
|
byteorder::{
|
||||||
BigEndian,
|
BigEndian,
|
||||||
ReadBytesExt,
|
ReadBytesExt,
|
||||||
WriteBytesExt,
|
WriteBytesExt,
|
||||||
},
|
},
|
||||||
|
cosmwasm_std::Binary,
|
||||||
p2w_sdk::ErrBox,
|
p2w_sdk::ErrBox,
|
||||||
schemars::JsonSchema,
|
schemars::JsonSchema,
|
||||||
serde::{
|
serde::{
|
||||||
Deserialize,
|
Deserialize,
|
||||||
Serialize,
|
Serialize,
|
||||||
},
|
},
|
||||||
std::io::Write,
|
std::{
|
||||||
|
convert::TryFrom,
|
||||||
|
io::Write,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
|
const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
|
||||||
|
@ -46,14 +57,14 @@ impl GovernanceModule {
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum GovernanceAction {
|
pub enum GovernanceAction {
|
||||||
UpgradeContract, // 0
|
UpgradeContract { address: [u8; 20] }, // 0
|
||||||
AuthorizeGovernanceDataSourceTransfer, // 1
|
AuthorizeGovernanceDataSourceTransfer { claim_vaa: Binary }, // 1
|
||||||
SetDataSources, // 2
|
SetDataSources { data_sources: Vec<PythDataSource> }, // 2
|
||||||
// Set the fee to val * (10 ** expo)
|
// Set the fee to val * (10 ** expo)
|
||||||
SetFee { val: u64, expo: u64 }, // 3
|
SetFee { val: u64, expo: u64 }, // 3
|
||||||
// Set the default valid period to the provided number of seconds
|
// Set the default valid period to the provided number of seconds
|
||||||
SetValidPeriod { valid_seconds: u64 }, // 4
|
SetValidPeriod { valid_seconds: u64 }, // 4
|
||||||
RequestGovernanceDataSourceTransfer, // 5
|
RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 }, // 5
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -86,13 +97,50 @@ impl GovernanceInstruction {
|
||||||
let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
|
let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
|
||||||
|
|
||||||
let action: Result<GovernanceAction, String> = match action_type {
|
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 => {
|
3 => {
|
||||||
let val = bytes.read_u64::<BigEndian>()?;
|
let val = bytes.read_u64::<BigEndian>()?;
|
||||||
let expo = bytes.read_u64::<BigEndian>()?;
|
let expo = bytes.read_u64::<BigEndian>()?;
|
||||||
Ok(GovernanceAction::SetFee { val, expo })
|
Ok(GovernanceAction::SetFee { val, expo })
|
||||||
}
|
}
|
||||||
// TODO: add parsing for additional actions
|
4 => {
|
||||||
_ => Err(format!("Bad governance action {action_type}",)),
|
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 {
|
Ok(GovernanceInstruction {
|
||||||
|
@ -109,17 +157,32 @@ impl GovernanceInstruction {
|
||||||
buf.write_u8(self.module.to_u8())?;
|
buf.write_u8(self.module.to_u8())?;
|
||||||
|
|
||||||
match &self.action {
|
match &self.action {
|
||||||
GovernanceAction::UpgradeContract => {
|
GovernanceAction::UpgradeContract { address } => {
|
||||||
buf.write_u8(0)?;
|
buf.write_u8(0)?;
|
||||||
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
||||||
|
buf.write_all(address)?;
|
||||||
}
|
}
|
||||||
GovernanceAction::AuthorizeGovernanceDataSourceTransfer => {
|
GovernanceAction::AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
|
||||||
buf.write_u8(1)?;
|
buf.write_u8(1)?;
|
||||||
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
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_u8(2)?;
|
||||||
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
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 } => {
|
GovernanceAction::SetFee { val, expo } => {
|
||||||
buf.write_u8(3)?;
|
buf.write_u8(3)?;
|
||||||
|
@ -136,9 +199,12 @@ impl GovernanceInstruction {
|
||||||
|
|
||||||
buf.write_u64::<BigEndian>(*new_valid_period)?;
|
buf.write_u64::<BigEndian>(*new_valid_period)?;
|
||||||
}
|
}
|
||||||
GovernanceAction::RequestGovernanceDataSourceTransfer => {
|
GovernanceAction::RequestGovernanceDataSourceTransfer {
|
||||||
|
governance_data_source_index,
|
||||||
|
} => {
|
||||||
buf.write_u8(5)?;
|
buf.write_u8(5)?;
|
||||||
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
buf.write_u16::<BigEndian>(self.target_chain_id)?;
|
||||||
|
buf.write_u32::<BigEndian>(*governance_data_source_index)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use {
|
use {
|
||||||
crate::state::PythDataSource,
|
|
||||||
cosmwasm_std::{
|
cosmwasm_std::{
|
||||||
Binary,
|
Binary,
|
||||||
Uint128,
|
Uint128,
|
||||||
|
@ -26,6 +25,7 @@ pub struct InstantiateMsg {
|
||||||
pub pyth_emitter_chain: u16,
|
pub pyth_emitter_chain: u16,
|
||||||
pub governance_emitter: Binary,
|
pub governance_emitter: Binary,
|
||||||
pub governance_emitter_chain: u16,
|
pub governance_emitter_chain: u16,
|
||||||
|
pub governance_source_index: u32,
|
||||||
pub governance_sequence_number: u64,
|
pub governance_sequence_number: u64,
|
||||||
pub chain_id: u16,
|
pub chain_id: u16,
|
||||||
pub valid_time_period_secs: u16,
|
pub valid_time_period_secs: u16,
|
||||||
|
@ -39,8 +39,6 @@ pub struct InstantiateMsg {
|
||||||
pub enum ExecuteMsg {
|
pub enum ExecuteMsg {
|
||||||
// TODO: add UpdatePriceFeeds if necessary
|
// TODO: add UpdatePriceFeeds if necessary
|
||||||
UpdatePriceFeeds { data: Binary },
|
UpdatePriceFeeds { data: Binary },
|
||||||
AddDataSource { data_source: PythDataSource },
|
|
||||||
RemoveDataSource { data_source: PythDataSource },
|
|
||||||
ExecuteGovernanceInstruction { data: Binary },
|
ExecuteGovernanceInstruction { data: Binary },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,13 +37,22 @@ pub struct PythDataSource {
|
||||||
pub pyth_emitter_chain: u16,
|
pub pyth_emitter_chain: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardian set information
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||||
pub struct ConfigInfo {
|
pub struct ConfigInfo {
|
||||||
pub owner: Addr,
|
pub owner: Addr,
|
||||||
pub wormhole_contract: Addr,
|
pub wormhole_contract: Addr,
|
||||||
pub data_sources: HashSet<PythDataSource>,
|
pub data_sources: HashSet<PythDataSource>,
|
||||||
pub governance_source: 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,
|
pub governance_sequence_number: u64,
|
||||||
// FIXME: This id needs to agree with the wormhole chain id.
|
// FIXME: This id needs to agree with the wormhole chain id.
|
||||||
// We should read this directly from wormhole.
|
// We should read this directly from wormhole.
|
||||||
|
|
|
@ -180,6 +180,7 @@ addresses["pyth_cosmwasm.wasm"] = await instantiate(
|
||||||
"hex"
|
"hex"
|
||||||
).toString("base64"),
|
).toString("base64"),
|
||||||
governance_emitter_chain: pythChain,
|
governance_emitter_chain: pythChain,
|
||||||
|
governance_source_index: 0,
|
||||||
governance_sequence_number: 0,
|
governance_sequence_number: 0,
|
||||||
chain_id: 3,
|
chain_id: 3,
|
||||||
valid_time_period_secs: 60,
|
valid_time_period_secs: 60,
|
||||||
|
|
Loading…
Reference in New Issue