add docs for audit (#645)
Co-authored-by: Jayant Krishnamurthy <jkrishnamurthy@jumptrading.com>
This commit is contained in:
parent
c3ca23f3b6
commit
66783e5ef1
|
@ -37,3 +37,8 @@ Available price feeds on these networks can be find below:
|
||||||
| Network | Available Price Feeds |
|
| Network | Available Price Feeds |
|
||||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| Injective Testnet | [https://pyth.network/developers/price-feed-ids#injective-testnet](https://pyth.network/developers/price-feed-ids#injective-testnet) |
|
| Injective Testnet | [https://pyth.network/developers/price-feed-ids#injective-testnet](https://pyth.network/developers/price-feed-ids#injective-testnet) |
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
The cosmwasm contract lives in the `pyth` subdirectory.
|
||||||
|
From that directory, you can build the contract with `cargo build` and run unit tests with `cargo test`.
|
||||||
|
|
|
@ -10,6 +10,7 @@ use {
|
||||||
UpgradeContract,
|
UpgradeContract,
|
||||||
},
|
},
|
||||||
GovernanceInstruction,
|
GovernanceInstruction,
|
||||||
|
GovernanceModule,
|
||||||
},
|
},
|
||||||
msg::{
|
msg::{
|
||||||
InstantiateMsg,
|
InstantiateMsg,
|
||||||
|
@ -112,7 +113,12 @@ pub fn instantiate(
|
||||||
Ok(Response::default())
|
Ok(Response::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
|
/// Verify that `data` represents an authentic Wormhole VAA.
|
||||||
|
///
|
||||||
|
/// *Warning* this function does not verify the emitter of the wormhole message; it only checks
|
||||||
|
/// that the wormhole signatures are valid. The caller is responsible for checking that the message
|
||||||
|
/// originates from the expected emitter.
|
||||||
|
pub fn parse_and_verify_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
|
||||||
let cfg = config_read(deps.storage).load()?;
|
let cfg = config_read(deps.storage).load()?;
|
||||||
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
|
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
|
||||||
contract_addr: cfg.wormhole_contract.to_string(),
|
contract_addr: cfg.wormhole_contract.to_string(),
|
||||||
|
@ -134,6 +140,11 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the on-chain price feeds given the array of price update VAAs `data`.
|
||||||
|
/// Each price update VAA must be a valid Wormhole message and sent from an authorized emitter.
|
||||||
|
///
|
||||||
|
/// This method additionally requires the caller to pay a fee to the contract; the
|
||||||
|
/// magnitude of the fee depends on both the data and the current contract configuration.
|
||||||
fn update_price_feeds(
|
fn update_price_feeds(
|
||||||
mut deps: DepsMut,
|
mut deps: DepsMut,
|
||||||
env: Env,
|
env: Env,
|
||||||
|
@ -142,6 +153,7 @@ fn update_price_feeds(
|
||||||
) -> StdResult<Response> {
|
) -> StdResult<Response> {
|
||||||
let state = config_read(deps.storage).load()?;
|
let state = config_read(deps.storage).load()?;
|
||||||
|
|
||||||
|
// Check that a sufficient fee was sent with the message
|
||||||
if state.fee.amount.u128() > 0
|
if state.fee.amount.u128() > 0
|
||||||
&& !has_coins(info.funds.as_ref(), &get_update_fee(&deps.as_ref(), data)?)
|
&& !has_coins(info.funds.as_ref(), &get_update_fee(&deps.as_ref(), data)?)
|
||||||
{
|
{
|
||||||
|
@ -151,7 +163,7 @@ fn update_price_feeds(
|
||||||
let mut total_attestations: usize = 0;
|
let mut total_attestations: usize = 0;
|
||||||
let mut new_attestations: usize = 0;
|
let mut new_attestations: usize = 0;
|
||||||
for datum in data {
|
for datum in data {
|
||||||
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), datum)?;
|
let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), datum)?;
|
||||||
verify_vaa_from_data_source(&state, &vaa)?;
|
verify_vaa_from_data_source(&state, &vaa)?;
|
||||||
|
|
||||||
let data = &vaa.payload;
|
let data = &vaa.payload;
|
||||||
|
@ -170,19 +182,24 @@ fn update_price_feeds(
|
||||||
.add_attribute("num_updated", format!("{new_attestations}")))
|
.add_attribute("num_updated", format!("{new_attestations}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a governance instruction provided as the VAA `data`.
|
||||||
|
/// The VAA must come from an authorized governance emitter.
|
||||||
|
/// See [GovernanceInstruction] for descriptions of the supported operations.
|
||||||
fn execute_governance_instruction(
|
fn execute_governance_instruction(
|
||||||
mut deps: DepsMut,
|
mut deps: DepsMut,
|
||||||
env: Env,
|
env: Env,
|
||||||
_info: MessageInfo,
|
_info: MessageInfo,
|
||||||
data: &Binary,
|
data: &Binary,
|
||||||
) -> StdResult<Response> {
|
) -> StdResult<Response> {
|
||||||
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
|
let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), data)?;
|
||||||
let state = config_read(deps.storage).load()?;
|
let state = config_read(deps.storage).load()?;
|
||||||
|
verify_vaa_from_governance_source(&state, &vaa)?;
|
||||||
|
|
||||||
// store updates to the config as a result of this action in here.
|
// store updates to the config as a result of this action in here.
|
||||||
let mut updated_config: ConfigInfo = state.clone();
|
let mut updated_config: ConfigInfo = state.clone();
|
||||||
verify_vaa_from_governance_source(&state, &vaa)?;
|
|
||||||
|
|
||||||
|
// Governance messages must be applied in order. This check prevents replay attacks where
|
||||||
|
// previous messages are re-applied.
|
||||||
if vaa.sequence <= state.governance_sequence_number {
|
if vaa.sequence <= state.governance_sequence_number {
|
||||||
return Err(PythContractError::OldGovernanceMessage)?;
|
return Err(PythContractError::OldGovernanceMessage)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -193,10 +210,18 @@ fn execute_governance_instruction(
|
||||||
let instruction = GovernanceInstruction::deserialize(&data[..])
|
let instruction = GovernanceInstruction::deserialize(&data[..])
|
||||||
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
|
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
|
||||||
|
|
||||||
|
// Check that the instruction is intended for this chain.
|
||||||
|
// chain_id = 0 means the instruction applies to all chains
|
||||||
if instruction.target_chain_id != state.chain_id && instruction.target_chain_id != 0 {
|
if instruction.target_chain_id != state.chain_id && instruction.target_chain_id != 0 {
|
||||||
return Err(PythContractError::InvalidGovernancePayload)?;
|
return Err(PythContractError::InvalidGovernancePayload)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that the instruction is intended for this target chain contract (as opposed to
|
||||||
|
// other Pyth contracts that may live on the same chain).
|
||||||
|
if instruction.module != GovernanceModule::Target {
|
||||||
|
return Err(PythContractError::InvalidGovernancePayload)?;
|
||||||
|
}
|
||||||
|
|
||||||
let response = match instruction.action {
|
let response = match instruction.action {
|
||||||
UpgradeContract { code_id } => {
|
UpgradeContract { code_id } => {
|
||||||
if instruction.target_chain_id == 0 {
|
if instruction.target_chain_id == 0 {
|
||||||
|
@ -205,7 +230,8 @@ fn execute_governance_instruction(
|
||||||
upgrade_contract(&env.contract.address, code_id)?
|
upgrade_contract(&env.contract.address, code_id)?
|
||||||
}
|
}
|
||||||
AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
|
AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
|
||||||
let parsed_claim_vaa = parse_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
|
let parsed_claim_vaa =
|
||||||
|
parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
|
||||||
transfer_governance(&mut updated_config, &state, &parsed_claim_vaa)?
|
transfer_governance(&mut updated_config, &state, &parsed_claim_vaa)?
|
||||||
}
|
}
|
||||||
SetDataSources { data_sources } => {
|
SetDataSources { data_sources } => {
|
||||||
|
@ -252,8 +278,9 @@ fn execute_governance_instruction(
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transfers governance to the data source provided in `parsed_claim_vaa`. Stores the new
|
/// Transfers governance to the data source provided in `parsed_claim_vaa`.
|
||||||
/// governance parameters in `next_config`.
|
/// This function updates the contract config in `next_config`; it is the caller's responsibility
|
||||||
|
/// to save this configuration in the on-chain storage.
|
||||||
fn transfer_governance(
|
fn transfer_governance(
|
||||||
next_config: &mut ConfigInfo,
|
next_config: &mut ConfigInfo,
|
||||||
current_config: &ConfigInfo,
|
current_config: &ConfigInfo,
|
||||||
|
@ -263,6 +290,10 @@ fn transfer_governance(
|
||||||
GovernanceInstruction::deserialize(parsed_claim_vaa.payload.as_slice())
|
GovernanceInstruction::deserialize(parsed_claim_vaa.payload.as_slice())
|
||||||
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
|
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
|
||||||
|
|
||||||
|
// Check that the requester is asking to govern this target chain contract.
|
||||||
|
// chain_id == 0 means they're asking for governance of all target chain contracts.
|
||||||
|
// (this check doesn't matter for security because we have already checked the information
|
||||||
|
// in the authorization message.)
|
||||||
if claim_vaa_instruction.target_chain_id != current_config.chain_id
|
if claim_vaa_instruction.target_chain_id != current_config.chain_id
|
||||||
&& claim_vaa_instruction.target_chain_id != 0
|
&& claim_vaa_instruction.target_chain_id != 0
|
||||||
{
|
{
|
||||||
|
@ -342,6 +373,7 @@ fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> Std
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the on-chain storage for any new price updates provided in `batch_attestation`.
|
||||||
fn process_batch_attestation(
|
fn process_batch_attestation(
|
||||||
deps: &mut DepsMut,
|
deps: &mut DepsMut,
|
||||||
env: &Env,
|
env: &Env,
|
||||||
|
@ -454,8 +486,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_price_feed(deps: &Deps, address: &[u8]) -> StdResult<PriceFeedResponse> {
|
/// Get the most recent value of the price feed indicated by `feed_id`.
|
||||||
match price_info_read(deps.storage).load(address) {
|
pub fn query_price_feed(deps: &Deps, feed_id: &[u8]) -> StdResult<PriceFeedResponse> {
|
||||||
|
match price_info_read(deps.storage).load(feed_id) {
|
||||||
Ok(price_info) => Ok(PriceFeedResponse {
|
Ok(price_info) => Ok(PriceFeedResponse {
|
||||||
price_feed: price_info.price_feed,
|
price_feed: price_info.price_feed,
|
||||||
}),
|
}),
|
||||||
|
@ -463,6 +496,8 @@ pub fn query_price_feed(deps: &Deps, address: &[u8]) -> StdResult<PriceFeedRespo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the fee that a caller must pay in order to submit a price update.
|
||||||
|
/// The fee depends on both the current contract configuration and the update data `vaas`.
|
||||||
pub fn get_update_fee(deps: &Deps, vaas: &[Binary]) -> StdResult<Coin> {
|
pub fn get_update_fee(deps: &Deps, vaas: &[Binary]) -> StdResult<Coin> {
|
||||||
let config = config_read(deps.storage).load()?;
|
let config = config_read(deps.storage).load()?;
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum GovernanceModule {
|
pub enum GovernanceModule {
|
||||||
/// The PythNet executor contract
|
/// The PythNet executor contract. Messages sent to the
|
||||||
Executor = 0,
|
Executor = 0,
|
||||||
/// A target chain contract (like this one!)
|
/// A target chain contract (like this one!)
|
||||||
Target = 1,
|
Target = 1,
|
||||||
|
@ -48,16 +48,33 @@ impl GovernanceModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The action to perform to change the state of the target chain contract.
|
/// The action to perform to change the state of the target chain contract.
|
||||||
|
///
|
||||||
|
/// Note that the order of the enum cannot be changed, as the integer representation of
|
||||||
|
/// each field must be preserved for backward compatibility.
|
||||||
#[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 {
|
||||||
|
/// Upgrade the code for the contract to the code uploaded at code_id
|
||||||
UpgradeContract { code_id: u64 }, // 0
|
UpgradeContract { code_id: u64 }, // 0
|
||||||
|
/// This action is the second step of a governance handoff process.
|
||||||
|
/// The handoff is as follows:
|
||||||
|
/// 1. The new governance emitter creates a VAA containing a RequestGovernanceDataSourceTransfer action
|
||||||
|
/// 2. The existing governance emitter creates a AuthorizeGovernanceDataSourceTransfer message where
|
||||||
|
/// claim_vaa is the VAA from step 1.
|
||||||
|
/// 3. The VAA from step 2 is submitted to the contract.
|
||||||
|
///
|
||||||
|
/// This 2-step process ensures that the new emitter is able to send VAAs before the transfer
|
||||||
|
/// is completed.
|
||||||
AuthorizeGovernanceDataSourceTransfer { claim_vaa: Binary }, // 1
|
AuthorizeGovernanceDataSourceTransfer { claim_vaa: Binary }, // 1
|
||||||
|
/// Set the set of authorized emitters for price update messages.
|
||||||
SetDataSources { data_sources: Vec<PythDataSource> }, // 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
|
||||||
|
/// The first step of the governance handoff process (see documentation
|
||||||
|
/// on AuthorizeGovernanceDataSourceTransfer). `governance_data_source_index` is an incrementing
|
||||||
|
/// sequence number that ensures old transfer messages cannot be replayed.
|
||||||
RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 }, // 5
|
RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 }, // 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,10 +100,6 @@ impl GovernanceInstruction {
|
||||||
let module_num = bytes.read_u8()?;
|
let module_num = bytes.read_u8()?;
|
||||||
let module = GovernanceModule::from_u8(module_num)?;
|
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 action_type: u8 = bytes.read_u8()?;
|
||||||
let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
|
let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,8 @@ use {
|
||||||
pub static CONFIG_KEY: &[u8] = b"config";
|
pub static CONFIG_KEY: &[u8] = b"config";
|
||||||
pub static PRICE_INFO_KEY: &[u8] = b"price_info_v4";
|
pub static PRICE_INFO_KEY: &[u8] = b"price_info_v4";
|
||||||
|
|
||||||
|
/// A `PythDataSource` identifies a specific contract (given by its Wormhole `emitter`) on
|
||||||
|
/// a specific blockchain (given by `chain_id`).
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
|
||||||
pub struct PythDataSource {
|
pub struct PythDataSource {
|
||||||
pub emitter: Binary,
|
pub emitter: Binary,
|
||||||
|
@ -54,8 +56,8 @@ pub struct ConfigInfo {
|
||||||
// governance messages, whereas the one above is generated by Pyth and only applicable to governance
|
// governance messages, whereas the one above is generated by Pyth and only applicable to governance
|
||||||
// source transfers.
|
// source transfers.
|
||||||
pub governance_sequence_number: u64,
|
pub governance_sequence_number: u64,
|
||||||
// FIXME: This id needs to agree with the wormhole chain id.
|
// Warning: This id needs to agree with the wormhole chain id.
|
||||||
// We should read this directly from wormhole.
|
// We should read this directly from wormhole, but their contract doesn't expose it.
|
||||||
pub chain_id: u16,
|
pub chain_id: u16,
|
||||||
pub valid_time_period: Duration,
|
pub valid_time_period: Duration,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue