Skip to content

Commit 66783e5

Browse files
jayantkJayant Krishnamurthy
andauthored
add docs for audit (#645)
Co-authored-by: Jayant Krishnamurthy <[email protected]>
1 parent c3ca23f commit 66783e5

File tree

4 files changed

+75
-20
lines changed

4 files changed

+75
-20
lines changed

target_chains/cosmwasm/contracts/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ Available price feeds on these networks can be find below:
3737
| Network | Available Price Feeds |
3838
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
3939
| Injective Testnet | [https://pyth.network/developers/price-feed-ids#injective-testnet](https://pyth.network/developers/price-feed-ids#injective-testnet) |
40+
41+
## Developing
42+
43+
The cosmwasm contract lives in the `pyth` subdirectory.
44+
From that directory, you can build the contract with `cargo build` and run unit tests with `cargo test`.

target_chains/cosmwasm/contracts/pyth/src/contract.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
UpgradeContract,
1111
},
1212
GovernanceInstruction,
13+
GovernanceModule,
1314
},
1415
msg::{
1516
InstantiateMsg,
@@ -112,7 +113,12 @@ pub fn instantiate(
112113
Ok(Response::default())
113114
}
114115

115-
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
116+
/// Verify that `data` represents an authentic Wormhole VAA.
117+
///
118+
/// *Warning* this function does not verify the emitter of the wormhole message; it only checks
119+
/// that the wormhole signatures are valid. The caller is responsible for checking that the message
120+
/// originates from the expected emitter.
121+
pub fn parse_and_verify_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
116122
let cfg = config_read(deps.storage).load()?;
117123
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
118124
contract_addr: cfg.wormhole_contract.to_string(),
@@ -134,6 +140,11 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
134140
}
135141
}
136142

143+
/// Update the on-chain price feeds given the array of price update VAAs `data`.
144+
/// Each price update VAA must be a valid Wormhole message and sent from an authorized emitter.
145+
///
146+
/// This method additionally requires the caller to pay a fee to the contract; the
147+
/// magnitude of the fee depends on both the data and the current contract configuration.
137148
fn update_price_feeds(
138149
mut deps: DepsMut,
139150
env: Env,
@@ -142,6 +153,7 @@ fn update_price_feeds(
142153
) -> StdResult<Response> {
143154
let state = config_read(deps.storage).load()?;
144155

156+
// Check that a sufficient fee was sent with the message
145157
if state.fee.amount.u128() > 0
146158
&& !has_coins(info.funds.as_ref(), &get_update_fee(&deps.as_ref(), data)?)
147159
{
@@ -151,7 +163,7 @@ fn update_price_feeds(
151163
let mut total_attestations: usize = 0;
152164
let mut new_attestations: usize = 0;
153165
for datum in data {
154-
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), datum)?;
166+
let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), datum)?;
155167
verify_vaa_from_data_source(&state, &vaa)?;
156168

157169
let data = &vaa.payload;
@@ -170,19 +182,24 @@ fn update_price_feeds(
170182
.add_attribute("num_updated", format!("{new_attestations}")))
171183
}
172184

185+
/// Execute a governance instruction provided as the VAA `data`.
186+
/// The VAA must come from an authorized governance emitter.
187+
/// See [GovernanceInstruction] for descriptions of the supported operations.
173188
fn execute_governance_instruction(
174189
mut deps: DepsMut,
175190
env: Env,
176191
_info: MessageInfo,
177192
data: &Binary,
178193
) -> StdResult<Response> {
179-
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
194+
let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), data)?;
180195
let state = config_read(deps.storage).load()?;
196+
verify_vaa_from_governance_source(&state, &vaa)?;
181197

182198
// store updates to the config as a result of this action in here.
183199
let mut updated_config: ConfigInfo = state.clone();
184-
verify_vaa_from_governance_source(&state, &vaa)?;
185200

201+
// Governance messages must be applied in order. This check prevents replay attacks where
202+
// previous messages are re-applied.
186203
if vaa.sequence <= state.governance_sequence_number {
187204
return Err(PythContractError::OldGovernanceMessage)?;
188205
} else {
@@ -193,10 +210,18 @@ fn execute_governance_instruction(
193210
let instruction = GovernanceInstruction::deserialize(&data[..])
194211
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
195212

213+
// Check that the instruction is intended for this chain.
214+
// chain_id = 0 means the instruction applies to all chains
196215
if instruction.target_chain_id != state.chain_id && instruction.target_chain_id != 0 {
197216
return Err(PythContractError::InvalidGovernancePayload)?;
198217
}
199218

219+
// Check that the instruction is intended for this target chain contract (as opposed to
220+
// other Pyth contracts that may live on the same chain).
221+
if instruction.module != GovernanceModule::Target {
222+
return Err(PythContractError::InvalidGovernancePayload)?;
223+
}
224+
200225
let response = match instruction.action {
201226
UpgradeContract { code_id } => {
202227
if instruction.target_chain_id == 0 {
@@ -205,7 +230,8 @@ fn execute_governance_instruction(
205230
upgrade_contract(&env.contract.address, code_id)?
206231
}
207232
AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
208-
let parsed_claim_vaa = parse_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
233+
let parsed_claim_vaa =
234+
parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
209235
transfer_governance(&mut updated_config, &state, &parsed_claim_vaa)?
210236
}
211237
SetDataSources { data_sources } => {
@@ -252,8 +278,9 @@ fn execute_governance_instruction(
252278
Ok(response)
253279
}
254280

255-
/// Transfers governance to the data source provided in `parsed_claim_vaa`. Stores the new
256-
/// governance parameters in `next_config`.
281+
/// Transfers governance to the data source provided in `parsed_claim_vaa`.
282+
/// This function updates the contract config in `next_config`; it is the caller's responsibility
283+
/// to save this configuration in the on-chain storage.
257284
fn transfer_governance(
258285
next_config: &mut ConfigInfo,
259286
current_config: &ConfigInfo,
@@ -263,6 +290,10 @@ fn transfer_governance(
263290
GovernanceInstruction::deserialize(parsed_claim_vaa.payload.as_slice())
264291
.map_err(|_| PythContractError::InvalidGovernancePayload)?;
265292

293+
// Check that the requester is asking to govern this target chain contract.
294+
// chain_id == 0 means they're asking for governance of all target chain contracts.
295+
// (this check doesn't matter for security because we have already checked the information
296+
// in the authorization message.)
266297
if claim_vaa_instruction.target_chain_id != current_config.chain_id
267298
&& claim_vaa_instruction.target_chain_id != 0
268299
{
@@ -342,6 +373,7 @@ fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> Std
342373
Ok(())
343374
}
344375

376+
/// Update the on-chain storage for any new price updates provided in `batch_attestation`.
345377
fn process_batch_attestation(
346378
deps: &mut DepsMut,
347379
env: &Env,
@@ -454,15 +486,18 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
454486
}
455487
}
456488

457-
pub fn query_price_feed(deps: &Deps, address: &[u8]) -> StdResult<PriceFeedResponse> {
458-
match price_info_read(deps.storage).load(address) {
489+
/// Get the most recent value of the price feed indicated by `feed_id`.
490+
pub fn query_price_feed(deps: &Deps, feed_id: &[u8]) -> StdResult<PriceFeedResponse> {
491+
match price_info_read(deps.storage).load(feed_id) {
459492
Ok(price_info) => Ok(PriceFeedResponse {
460493
price_feed: price_info.price_feed,
461494
}),
462495
Err(_) => Err(PythContractError::PriceFeedNotFound)?,
463496
}
464497
}
465498

499+
/// Get the fee that a caller must pay in order to submit a price update.
500+
/// The fee depends on both the current contract configuration and the update data `vaas`.
466501
pub fn get_update_fee(deps: &Deps, vaas: &[Binary]) -> StdResult<Coin> {
467502
let config = config_read(deps.storage).load()?;
468503

target_chains/cosmwasm/contracts/pyth/src/governance.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const PYTH_GOVERNANCE_MAGIC: &[u8] = b"PTGM";
2424
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
2525
#[repr(u8)]
2626
pub enum GovernanceModule {
27-
/// The PythNet executor contract
27+
/// The PythNet executor contract. Messages sent to the
2828
Executor = 0,
2929
/// A target chain contract (like this one!)
3030
Target = 1,
@@ -48,16 +48,33 @@ impl GovernanceModule {
4848
}
4949

5050
/// The action to perform to change the state of the target chain contract.
51+
///
52+
/// Note that the order of the enum cannot be changed, as the integer representation of
53+
/// each field must be preserved for backward compatibility.
5154
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
5255
#[repr(u8)]
5356
pub enum GovernanceAction {
54-
UpgradeContract { code_id: u64 }, // 0
57+
/// Upgrade the code for the contract to the code uploaded at code_id
58+
UpgradeContract { code_id: u64 }, // 0
59+
/// This action is the second step of a governance handoff process.
60+
/// The handoff is as follows:
61+
/// 1. The new governance emitter creates a VAA containing a RequestGovernanceDataSourceTransfer action
62+
/// 2. The existing governance emitter creates a AuthorizeGovernanceDataSourceTransfer message where
63+
/// claim_vaa is the VAA from step 1.
64+
/// 3. The VAA from step 2 is submitted to the contract.
65+
///
66+
/// This 2-step process ensures that the new emitter is able to send VAAs before the transfer
67+
/// is completed.
5568
AuthorizeGovernanceDataSourceTransfer { claim_vaa: Binary }, // 1
56-
SetDataSources { data_sources: Vec<PythDataSource> }, // 2
57-
// Set the fee to val * (10 ** expo)
69+
/// Set the set of authorized emitters for price update messages.
70+
SetDataSources { data_sources: Vec<PythDataSource> }, // 2
71+
/// Set the fee to val * (10 ** expo)
5872
SetFee { val: u64, expo: u64 }, // 3
59-
// Set the default valid period to the provided number of seconds
73+
/// Set the default valid period to the provided number of seconds
6074
SetValidPeriod { valid_seconds: u64 }, // 4
75+
/// The first step of the governance handoff process (see documentation
76+
/// on AuthorizeGovernanceDataSourceTransfer). `governance_data_source_index` is an incrementing
77+
/// sequence number that ensures old transfer messages cannot be replayed.
6178
RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 }, // 5
6279
}
6380

@@ -83,10 +100,6 @@ impl GovernanceInstruction {
83100
let module_num = bytes.read_u8()?;
84101
let module = GovernanceModule::from_u8(module_num)?;
85102

86-
if module != GovernanceModule::Target {
87-
return Err(format!("Invalid governance module {module_num}",).into());
88-
}
89-
90103
let action_type: u8 = bytes.read_u8()?;
91104
let target_chain_id: u16 = bytes.read_u16::<BigEndian>()?;
92105

target_chains/cosmwasm/contracts/pyth/src/state.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ use {
3131
pub static CONFIG_KEY: &[u8] = b"config";
3232
pub static PRICE_INFO_KEY: &[u8] = b"price_info_v4";
3333

34+
/// A `PythDataSource` identifies a specific contract (given by its Wormhole `emitter`) on
35+
/// a specific blockchain (given by `chain_id`).
3436
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
3537
pub struct PythDataSource {
3638
pub emitter: Binary,
@@ -54,8 +56,8 @@ pub struct ConfigInfo {
5456
// governance messages, whereas the one above is generated by Pyth and only applicable to governance
5557
// source transfers.
5658
pub governance_sequence_number: u64,
57-
// FIXME: This id needs to agree with the wormhole chain id.
58-
// We should read this directly from wormhole.
59+
// Warning: This id needs to agree with the wormhole chain id.
60+
// We should read this directly from wormhole, but their contract doesn't expose it.
5961
pub chain_id: u16,
6062
pub valid_time_period: Duration,
6163

0 commit comments

Comments
 (0)