From 9303a211b7e85b4f7d7bcf653c431b7f119b8b33 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 22 Mar 2023 17:13:49 +0000 Subject: [PATCH 01/32] state getters and setters, change Move.toml dependency to sui/integration_v2 --- target_chains/sui/contracts/Move.toml | 2 +- .../sources/batch_price_attestation.move | 248 ++++++++++++++++++ target_chains/sui/contracts/sources/pyth.move | 1 + .../sui/contracts/sources/state.move | 124 +++++++++ 4 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 target_chains/sui/contracts/sources/batch_price_attestation.move create mode 100644 target_chains/sui/contracts/sources/pyth.move create mode 100644 target_chains/sui/contracts/sources/state.move diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index 52411cac74..7db7a62dd3 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -10,7 +10,7 @@ rev = "157ac72030d014f17d76cefe81f3915b4afab2c9" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" subdir = "sui/wormhole" -rev = "sui/wormhole-cleanup" +rev = "sui/integration_v2" [addresses] pyth = "0x250" diff --git a/target_chains/sui/contracts/sources/batch_price_attestation.move b/target_chains/sui/contracts/sources/batch_price_attestation.move new file mode 100644 index 0000000000..5021535435 --- /dev/null +++ b/target_chains/sui/contracts/sources/batch_price_attestation.move @@ -0,0 +1,248 @@ +module pyth::batch_price_attestation { + + use sui::tx_context::{Self, TxContext}; + + use pyth::price_feed::{Self}; + use pyth::price_info::{Self, PriceInfo}; + use pyth::price_identifier::{Self}; + use pyth::price_status; + use pyth::deserialize::{Self}; + // TODO - Import Sui clock and use it for timekeeping instead of tx_context::epoch. + // Replace epoch in deserialize_price_info with sui clock timestamp, and usage + // of epoch in test_deserialize_batch_price_attestation. + // TODO - Use specific error messages in this module, specifically + // for invalid_attestation_magic_value and invalid_batch_attestation_header_size. + use wormhole::cursor::{Self, Cursor}; + use wormhole::bytes::{Self}; + + use std::vector::{Self}; + + #[test_only] + use pyth::price; + #[test_only] + use pyth::i64; + + const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes + + struct BatchPriceAttestation { + header: Header, + attestation_size: u64, + attestation_count: u64, + price_infos: vector, + } + + struct Header { + magic: u64, + version_major: u64, + version_minor: u64, + header_size: u64, + payload_id: u8, + } + + fun deserialize_header(cur: &mut Cursor): Header { + let magic = (deserialize::deserialize_u32(cur) as u64); + assert!(magic == MAGIC, 0); // TODO - add specific error value - error::invalid_attestation_magic_value() + let version_major = deserialize::deserialize_u16(cur); + let version_minor = deserialize::deserialize_u16(cur); + let header_size = deserialize::deserialize_u16(cur); + let payload_id = deserialize::deserialize_u8(cur); + + assert!(header_size >= 1, 0); // TODO - add specific error value - error::invalid_batch_attestation_header_size() + let unknown_header_bytes = header_size - 1; + let _unknown = bytes::take_bytes(cur, (unknown_header_bytes as u64)); + + Header { + magic: magic, + header_size: (header_size as u64), + version_minor: (version_minor as u64), + version_major: (version_major as u64), + payload_id: payload_id, + } + } + + public fun destroy(batch: BatchPriceAttestation): vector { + let BatchPriceAttestation { + header: Header { + magic: _, + version_major: _, + version_minor: _, + header_size: _, + payload_id: _, + }, + attestation_size: _, + attestation_count: _, + price_infos, + } = batch; + price_infos + } + + public fun get_attestation_count(batch: &BatchPriceAttestation): u64 { + batch.attestation_count + } + + public fun get_price_info(batch: &BatchPriceAttestation, index: u64): &PriceInfo { + vector::borrow(&batch.price_infos, index) + } + + public fun deserialize(bytes: vector, ctx: &mut TxContext): BatchPriceAttestation { + let cur = cursor::new(bytes); + let header = deserialize_header(&mut cur); + + let attestation_count = deserialize::deserialize_u16(&mut cur); + let attestation_size = deserialize::deserialize_u16(&mut cur); + let price_infos = vector::empty(); + + let i = 0; + while (i < attestation_count) { + let price_info = deserialize_price_info(&mut cur, ctx); + vector::push_back(&mut price_infos, price_info); + + // Consume any excess bytes + let parsed_bytes = 32+32+8+8+4+8+8+1+4+4+8+8+8+8+8; + let _excess = bytes::take_bytes(&mut cur, (attestation_size - parsed_bytes as u64)); + + i = i + 1; + }; + cursor::destroy_empty(cur); + + BatchPriceAttestation { + header, + attestation_count: (attestation_count as u64), + attestation_size: (attestation_size as u64), + price_infos: price_infos, + } + } + + fun deserialize_price_info(cur: &mut Cursor, ctx: &mut TxContext): PriceInfo { + + // Skip obselete field + let _product_identifier = deserialize::deserialize_vector(cur, 32); + let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(cur, 32)); + let price = deserialize::deserialize_i64(cur); + let conf = deserialize::deserialize_u64(cur); + let expo = deserialize::deserialize_i32(cur); + let ema_price = deserialize::deserialize_i64(cur); + let ema_conf = deserialize::deserialize_u64(cur); + let status = price_status::from_u64((deserialize::deserialize_u8(cur) as u64)); + + // Skip obselete fields + let _num_publishers = deserialize::deserialize_u32(cur); + let _max_num_publishers = deserialize::deserialize_u32(cur); + + let attestation_time = deserialize::deserialize_u64(cur); + let publish_time = deserialize::deserialize_u64(cur); // + let prev_publish_time = deserialize::deserialize_u64(cur); + let prev_price = deserialize::deserialize_i64(cur); + let prev_conf = deserialize::deserialize_u64(cur); + + // Handle the case where the status is not trading. This logic will soon be moved into + // the attester. + + // If status is trading, use the current price. + // If not, use the the last known trading price. + let current_price = pyth::price::new(price, conf, expo, publish_time); + if (status != price_status::new_trading()) { + current_price = pyth::price::new(prev_price, prev_conf, expo, prev_publish_time); + }; + + // If status is trading, use the timestamp of the aggregate as the timestamp for the + // EMA price. If not, the EMA will have last been updated when the aggregate last had + // trading status, so use prev_publish_time (the time when the aggregate last had trading status). + let ema_timestamp = publish_time; + if (status != price_status::new_trading()) { + ema_timestamp = prev_publish_time; + }; + + price_info::new( + attestation_time, + tx_context::epoch(ctx), //TODO - use Sui Clock to get timestamp in seconds + price_feed::new( + price_identifier, + current_price, + pyth::price::new(ema_price, ema_conf, expo, ema_timestamp), + ) + ) + } + + #[test] + #[expected_failure] + fun test_deserialize_batch_price_attestation_invalid_magic() { + use sui::test_scenario::{Self, ctx}; + let test = test_scenario::begin(@0x1234); + + // A batch price attestation with a magic number of 0x50325749 + let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; + destroy(deserialize(bytes, ctx(&mut test))); + test_scenario::end(test); + } + + #[test] + fun test_deserialize_batch_price_attestation() { + use sui::test_scenario::{Self, ctx}; + // Set the arrival time + let test = test_scenario::begin(@0x1234); + let arrival_time = tx_context::epoch(ctx(&mut test)); + + // A raw batch price attestation + // The first attestation has a status of UNKNOWN + let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; + + let expected = BatchPriceAttestation { + header: Header { + magic: 0x50325748, + version_major: 3, + version_minor: 0, + payload_id: 2, + header_size: 1, + }, + attestation_count: 4, + attestation_size: 149, + price_infos: vector[ + price_info::new( + 1663680747, + arrival_time, + price_feed::new( + price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"), + price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740), + price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740), + ), + ), + price_info::new( + 1663680747, + arrival_time, + price_feed::new( + price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"), + price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745), + price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745), + ), + ), + price_info::new( + 1663680747, + arrival_time, + price_feed::new( + price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"), + price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745), + price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745), + ), + ), + price_info::new( + 1663680747, + arrival_time, + price_feed::new( + price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"), + price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745), + price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745), + ), + ), + ], + }; + + let deserialized = deserialize(bytes, ctx(&mut test)); + + assert!(&expected == &deserialized, 1); + destroy(expected); + destroy(deserialized); + + test_scenario::end(test); + } +} diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move new file mode 100644 index 0000000000..c8779aa927 --- /dev/null +++ b/target_chains/sui/contracts/sources/pyth.move @@ -0,0 +1 @@ +module pyth::pyth {} \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move new file mode 100644 index 0000000000..d51732171a --- /dev/null +++ b/target_chains/sui/contracts/sources/state.move @@ -0,0 +1,124 @@ +module pyth::state { + use std::vector; + use sui::object::{Self, UID}; + use sui::transfer::{Self}; + use sui::tx_context::{Self, TxContext}; + use sui::test_scenario::{Self}; + + //use pyth::price_identifier::PriceIdentifier; + use pyth::data_source::{Self, DataSource}; + //use pyth::price_info::PriceInfo; + + use wormhole::set::{Self, Set}; + use wormhole::external_address::{Self}; + + friend pyth::pyth; + + struct State has key { + id: UID, + // TODO - Make data_sources a dynamic field of State, + // inside of something embedded in State, because there will be + // 10k+ data sources in the future, and we want to minimize the + // size of State. + data_sources: Set, + governance_data_source: DataSource, + last_executed_governance_sequence: u64, + stale_price_threshold: u64, + base_update_fee: u64 + } + + // Initialization + public(friend) fun init_and_share_state( + stale_price_threshold: u64, + base_update_fee: u64, + governance_data_source: DataSource, + sources: vector, + ctx: &mut TxContext + ) { + // Convert the vector of DataSource objects into a set + // of DataSource objects + let data_sources = set::new(ctx); + while (!vector::is_empty(&sources)) { + set::add(&mut data_sources, vector::pop_back(&mut sources)); + }; + transfer::share_object( + State { + id: object::new(ctx), + data_sources, + governance_data_source, + last_executed_governance_sequence: 0, + stale_price_threshold, + base_update_fee + } + ); + } + + // Accessors + public fun get_stale_price_threshold_secs(s: &State): u64 { + s.stale_price_threshold + } + + public fun get_base_update_fee(s: &State): u64 { + s.base_update_fee + } + + public fun is_valid_data_source(s: &State, data_source: DataSource): bool { + set::contains(&s.data_sources, data_source) + } + + public fun is_valid_governance_data_source(s: &State, source: DataSource): bool { + s.governance_data_source == source + } + + public fun get_last_executed_governance_sequence(s: &State): u64 { + s.last_executed_governance_sequence + } + + // Setters + public(friend) fun set_data_sources(s: &mut State, new_sources: vector, ctx: &mut TxContext) { + //let sources = &mut s.data_sources; + let new_set = set::new(ctx); + //set::empty(sources); + while (!vector::is_empty(&new_sources)) { + set::add(&mut new_set, vector::pop_back(&mut new_sources)); + }; + s.data_sources = new_set; + } + + // public(friend) fun set_latest_price_info(price_identifier: PriceIdentifier, price_info: PriceInfo) acquires LatestPriceInfo { + // let latest_price_info = borrow_global_mut(@pyth); + // table::upsert(&mut latest_price_info.info, price_identifier, price_info) + // } + + // public(friend) fun set_last_executed_governance_sequence(sequence: u64) acquires LastExecutedGovernanceSequence { + // let last_executed_governance_sequence = borrow_global_mut(@pyth); + // last_executed_governance_sequence.sequence = sequence + // } + + // public(friend) fun pyth_signer(): signer acquires SignerCapability { + // account::create_signer_with_capability(&borrow_global(@pyth).signer_capability) + // } + + // public(friend) fun set_contract_upgrade_authorized_hash(hash: Hash) acquires ContractUpgradeAuthorized, SignerCapability { + // if (exists(@pyth)) { + // let ContractUpgradeAuthorized { hash: _ } = move_from(@pyth); + // }; + + // move_to(&pyth_signer(), ContractUpgradeAuthorized { hash }); + // } + + // public(friend) fun set_governance_data_source(source: DataSource) acquires GovernanceDataSource { + // let valid_governance_data_source = borrow_global_mut(@pyth); + // valid_governance_data_source.source = source; + // } + + // public(friend) fun set_base_update_fee(fee: u64) acquires BaseUpdateFee { + // let update_fee = borrow_global_mut(@pyth); + // update_fee.fee = fee + // } + + // public(friend) fun set_stale_price_threshold_secs(threshold_secs: u64) acquires StalePriceThreshold { + // let stale_price_threshold = borrow_global_mut(@pyth); + // stale_price_threshold.threshold_secs = threshold_secs + // } +} \ No newline at end of file From b69917ea3a63be63babfc17fafdd877e952e2a09 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 22 Mar 2023 17:36:53 +0000 Subject: [PATCH 02/32] finish state.move --- target_chains/sui/contracts/sources/set.move | 46 ++++++++++++ .../sui/contracts/sources/state.move | 70 ++++++------------- 2 files changed, 68 insertions(+), 48 deletions(-) create mode 100644 target_chains/sui/contracts/sources/set.move diff --git a/target_chains/sui/contracts/sources/set.move b/target_chains/sui/contracts/sources/set.move new file mode 100644 index 0000000000..b0f53549c8 --- /dev/null +++ b/target_chains/sui/contracts/sources/set.move @@ -0,0 +1,46 @@ +/// A set data structure. +module pyth::set { + use sui::table::{Self, Table}; + use sui::tx_context::{TxContext}; + use std::vector; + + /// Empty struct. Used as the value type in mappings to encode a set + struct Unit has store, copy, drop {} + + /// A set containing elements of type `A` with support for membership + /// checking. + struct Set has store { + keys: vector, + elems: Table + } + + /// Create a new Set. + public fun new(ctx: &mut TxContext): Set { + Set { + keys: vector::empty(), + elems: table::new(ctx), + } + } + + /// Add a new element to the set. + /// Aborts if the element already exists + public fun add(set: &mut Set, key: A) { + table::add(&mut set.elems, key, Unit {}); + vector::push_back(&mut set.keys, key); + } + + /// Returns true iff `set` contains an entry for `key`. + public fun contains(set: &Set, key: A): bool { + table::contains(&set.elems, key) + } + + /// Removes all elements from the set + public fun empty(set: &mut Set) { + while (!vector::is_empty(&set.keys)) { + table::remove(&mut set.elems, vector::pop_back(&mut set.keys)); + } + } + + // TODO: destroy_empty, but this is tricky because std::table doesn't + // have this functionality. +} diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index d51732171a..d0a3009d13 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -2,15 +2,10 @@ module pyth::state { use std::vector; use sui::object::{Self, UID}; use sui::transfer::{Self}; - use sui::tx_context::{Self, TxContext}; - use sui::test_scenario::{Self}; + use sui::tx_context::{TxContext}; - //use pyth::price_identifier::PriceIdentifier; - use pyth::data_source::{Self, DataSource}; - //use pyth::price_info::PriceInfo; - - use wormhole::set::{Self, Set}; - use wormhole::external_address::{Self}; + use pyth::data_source::{DataSource}; + use pyth::set::{Self, Set}; friend pyth::pyth; @@ -75,50 +70,29 @@ module pyth::state { } // Setters - public(friend) fun set_data_sources(s: &mut State, new_sources: vector, ctx: &mut TxContext) { - //let sources = &mut s.data_sources; - let new_set = set::new(ctx); - //set::empty(sources); + public(friend) fun set_data_sources(s: &mut State, new_sources: vector) { + // Empty the existing set of data sources instead of dropping it, + // because it does not have drop ability. + set::empty(&mut s.data_sources); + // Add new sources to state.data_sources. while (!vector::is_empty(&new_sources)) { - set::add(&mut new_set, vector::pop_back(&mut new_sources)); + set::add(&mut s.data_sources, vector::pop_back(&mut new_sources)); }; - s.data_sources = new_set; } - // public(friend) fun set_latest_price_info(price_identifier: PriceIdentifier, price_info: PriceInfo) acquires LatestPriceInfo { - // let latest_price_info = borrow_global_mut(@pyth); - // table::upsert(&mut latest_price_info.info, price_identifier, price_info) - // } - - // public(friend) fun set_last_executed_governance_sequence(sequence: u64) acquires LastExecutedGovernanceSequence { - // let last_executed_governance_sequence = borrow_global_mut(@pyth); - // last_executed_governance_sequence.sequence = sequence - // } - - // public(friend) fun pyth_signer(): signer acquires SignerCapability { - // account::create_signer_with_capability(&borrow_global(@pyth).signer_capability) - // } - - // public(friend) fun set_contract_upgrade_authorized_hash(hash: Hash) acquires ContractUpgradeAuthorized, SignerCapability { - // if (exists(@pyth)) { - // let ContractUpgradeAuthorized { hash: _ } = move_from(@pyth); - // }; - - // move_to(&pyth_signer(), ContractUpgradeAuthorized { hash }); - // } + public(friend) fun set_last_executed_governance_sequence(s: &mut State, sequence: u64) { + s.last_executed_governance_sequence = sequence; + } - // public(friend) fun set_governance_data_source(source: DataSource) acquires GovernanceDataSource { - // let valid_governance_data_source = borrow_global_mut(@pyth); - // valid_governance_data_source.source = source; - // } + public(friend) fun set_governance_data_source(s: &mut State, source: DataSource) { + s. governance_data_source = source; + } - // public(friend) fun set_base_update_fee(fee: u64) acquires BaseUpdateFee { - // let update_fee = borrow_global_mut(@pyth); - // update_fee.fee = fee - // } + public(friend) fun set_base_update_fee(s: &mut State, fee: u64) { + s.base_update_fee = fee; + } - // public(friend) fun set_stale_price_threshold_secs(threshold_secs: u64) acquires StalePriceThreshold { - // let stale_price_threshold = borrow_global_mut(@pyth); - // stale_price_threshold.threshold_secs = threshold_secs - // } -} \ No newline at end of file + public(friend) fun set_stale_price_threshold_secs(s: &mut State, threshold_secs: u64) { + s.stale_price_threshold = threshold_secs; + } +} From 0380a1f244b07d46e3271b9cad23a805eb1d65e8 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 22 Mar 2023 18:08:21 +0000 Subject: [PATCH 03/32] add new line to pyth --- target_chains/sui/contracts/sources/pyth.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index c8779aa927..01d7493657 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -1 +1 @@ -module pyth::pyth {} \ No newline at end of file +module pyth::pyth {} From e799fc60c5ee3e08056723bf5dbe2aa1a9c91b2d Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 22 Mar 2023 19:19:48 +0000 Subject: [PATCH 04/32] use deployer cap pattern for state module --- .../sui/contracts/sources/state.move | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index d0a3009d13..7e1cd73640 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -2,13 +2,19 @@ module pyth::state { use std::vector; use sui::object::{Self, UID}; use sui::transfer::{Self}; - use sui::tx_context::{TxContext}; + use sui::tx_context::{Self, TxContext}; use pyth::data_source::{DataSource}; use pyth::set::{Self, Set}; friend pyth::pyth; + /// Capability for creating a bridge state object, granted to sender when this + /// module is deployed + struct DeployerCap has key, store { + id: UID + } + struct State has key { id: UID, // TODO - Make data_sources a dynamic field of State, @@ -22,14 +28,27 @@ module pyth::state { base_update_fee: u64 } + fun init(ctx: &mut TxContext) { + transfer::transfer( + DeployerCap { + id: object::new(ctx) + }, + tx_context::sender(ctx) + ); + } + // Initialization public(friend) fun init_and_share_state( + deployer: DeployerCap, stale_price_threshold: u64, base_update_fee: u64, governance_data_source: DataSource, sources: vector, ctx: &mut TxContext ) { + let DeployerCap { id } = deployer; + object::delete(id); + // Convert the vector of DataSource objects into a set // of DataSource objects let data_sources = set::new(ctx); From 9ac886050e256d9538a28c186a51a0981c5027fc Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 22 Mar 2023 19:17:04 +0000 Subject: [PATCH 05/32] sui pyth --- target_chains/sui/contracts/sources/pyth.move | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 01d7493657..7f60d433f7 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -1 +1,56 @@ -module pyth::pyth {} +module pyth::pyth { + use std::vector; + use sui::tx_context::{TxContext}; + + use pyth::data_source::{Self, DataSource}; + //use pyth::set::{Self}; + use pyth::state::{Self}; + + use wormhole::external_address::{Self}; + + struct PythInitializationEvent has copy, drop {} + + fun init( + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + data_sources_emitter_chain_ids: vector, + data_sources_emitter_addresses: vector>, + update_fee: u64, + ctx: &mut TxContext + ) { + state::init_and_share_state( + stale_price_threshold, + update_fee, + data_source::new( + governance_emitter_chain_id, + external_address::from_bytes(governance_emitter_address)), + parse_data_sources( + data_sources_emitter_chain_ids, + data_sources_emitter_addresses, + ), + ctx + ) + } + + fun parse_data_sources( + emitter_chain_ids: vector, + emitter_addresses: vector>): vector { + + // TODO - add custom error type error::data_source_emitter_address_and_chain_ids_different_lengths() + assert!(vector::length(&emitter_chain_ids) == vector::length(&emitter_addresses), 0); + + let sources = vector::empty(); + let i = 0; + while (i < vector::length(&emitter_chain_ids)) { + vector::push_back(&mut sources, data_source::new( + *vector::borrow(&emitter_chain_ids, i), + external_address::from_bytes(*vector::borrow(&emitter_addresses, i)) + )); + + i = i + 1; + }; + sources + } + +} From 05d2121189b71fc4ede1deabd1796ee4c99f9977 Mon Sep 17 00:00:00 2001 From: optke3 Date: Thu, 23 Mar 2023 19:39:07 +0000 Subject: [PATCH 06/32] update price feeds, dynamic object fields, Sui object PriceInfoObject --- .../sources/batch_price_attestation.move | 24 +-- .../sui/contracts/sources/data_source.move | 42 ++++ .../sui/contracts/sources/event.move | 33 +++ .../sui/contracts/sources/price_feed.move | 14 +- .../sui/contracts/sources/price_info.move | 95 ++++++++- target_chains/sui/contracts/sources/pyth.move | 196 +++++++++++++++++- .../sui/contracts/sources/state.move | 73 ++++--- 7 files changed, 424 insertions(+), 53 deletions(-) create mode 100644 target_chains/sui/contracts/sources/event.move diff --git a/target_chains/sui/contracts/sources/batch_price_attestation.move b/target_chains/sui/contracts/sources/batch_price_attestation.move index 5021535435..f7ccd00ae7 100644 --- a/target_chains/sui/contracts/sources/batch_price_attestation.move +++ b/target_chains/sui/contracts/sources/batch_price_attestation.move @@ -153,7 +153,7 @@ module pyth::batch_price_attestation { ema_timestamp = prev_publish_time; }; - price_info::new( + price_info::new_price_info( attestation_time, tx_context::epoch(ctx), //TODO - use Sui Clock to get timestamp in seconds price_feed::new( @@ -172,7 +172,7 @@ module pyth::batch_price_attestation { // A batch price attestation with a magic number of 0x50325749 let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; - destroy(deserialize(bytes, ctx(&mut test))); + let _ = destroy(deserialize(bytes, ctx(&mut test))); test_scenario::end(test); } @@ -198,42 +198,38 @@ module pyth::batch_price_attestation { attestation_count: 4, attestation_size: 149, price_infos: vector[ - price_info::new( + price_info::new_price_info( 1663680747, arrival_time, price_feed::new( price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"), price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740), price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740), - ), - ), - price_info::new( + ) ), + price_info::new_price_info( 1663680747, arrival_time, price_feed::new( price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"), price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745), price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745), - ), - ), - price_info::new( + ) ), + price_info::new_price_info( 1663680747, arrival_time, price_feed::new( price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"), price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745), price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745), - ), - ), - price_info::new( + ) ), + price_info::new_price_info( 1663680747, arrival_time, price_feed::new( price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"), price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745), price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745), - ), - ), + ) ), ], }; diff --git a/target_chains/sui/contracts/sources/data_source.move b/target_chains/sui/contracts/sources/data_source.move index c79e548f22..c8b7af8266 100644 --- a/target_chains/sui/contracts/sources/data_source.move +++ b/target_chains/sui/contracts/sources/data_source.move @@ -1,11 +1,53 @@ module pyth::data_source { + use sui::dynamic_field::{Self}; + use sui::object::{UID}; + use sui::tx_context::{TxContext}; + + use pyth::set::{Self}; + use wormhole::external_address::ExternalAddress; + const KEY: vector = b"data_sources"; + struct DataSource has copy, drop, store { emitter_chain: u64, emitter_address: ExternalAddress, } + public fun new_data_source_registry(parent_id: &mut UID, ctx: &mut TxContext) { + assert!( + !dynamic_field::exists_(parent_id, KEY), + 0 // TODO - add custom error type + ); + dynamic_field::add( + parent_id, + KEY, + set::new(ctx) + ) + } + + public fun add(parent_id: &mut UID, data_source: DataSource) { + assert!( + !contains(parent_id, data_source), + 0 // TODO - add custom error message + ); + set::add( + dynamic_field::borrow_mut(parent_id, KEY), + data_source + ) + } + + public fun empty(parent_id: &mut UID){ + set::empty( + dynamic_field::borrow_mut(parent_id, KEY) + ) + } + + public fun contains(parent_id: &UID, data_source: DataSource): bool { + let ref = dynamic_field::borrow(parent_id, KEY); + set::contains(ref, data_source) + } + public fun new(emitter_chain: u64, emitter_address: ExternalAddress): DataSource { DataSource { emitter_chain: emitter_chain, diff --git a/target_chains/sui/contracts/sources/event.move b/target_chains/sui/contracts/sources/event.move new file mode 100644 index 0000000000..fadb33d4fa --- /dev/null +++ b/target_chains/sui/contracts/sources/event.move @@ -0,0 +1,33 @@ +module pyth::event { + use sui::event::{Self}; + use pyth::price_feed::{PriceFeed}; + + friend pyth::pyth; + friend pyth::state; + + struct PythInitializationEvent has copy, drop {} + + /// Signifies that a price feed has been updated + struct PriceFeedUpdate has copy, store, drop { + /// Value of the price feed + price_feed: PriceFeed, + /// Timestamp of the update + timestamp: u64, + } + + public(friend) fun emit_price_feed_update(price_feed: PriceFeed, timestamp: u64) { + event::emit( + PriceFeedUpdate { + price_feed, + timestamp, + } + ); + } + + public(friend) fun emit_pyth_initialization_event() { + event::emit( + PythInitializationEvent {} + ); + } + +} diff --git a/target_chains/sui/contracts/sources/price_feed.move b/target_chains/sui/contracts/sources/price_feed.move index 01ae10ba59..43ba535552 100644 --- a/target_chains/sui/contracts/sources/price_feed.move +++ b/target_chains/sui/contracts/sources/price_feed.move @@ -23,8 +23,18 @@ module pyth::price_feed { } } - public fun get_price_identifier(price_feed: &PriceFeed): &PriceIdentifier { - &price_feed.price_identifier + public fun from( + price_feed: &PriceFeed + ): PriceFeed { + PriceFeed { + price_identifier: price_feed.price_identifier, + price: price_feed.price, + ema_price: price_feed.ema_price, + } + } + + public fun get_price_identifier(price_feed: &PriceFeed): PriceIdentifier { + price_feed.price_identifier } public fun get_price(price_feed: &PriceFeed): Price { diff --git a/target_chains/sui/contracts/sources/price_info.move b/target_chains/sui/contracts/sources/price_info.move index e533fc9bfd..d49695d30b 100644 --- a/target_chains/sui/contracts/sources/price_info.move +++ b/target_chains/sui/contracts/sources/price_info.move @@ -1,13 +1,76 @@ module pyth::price_info { - use pyth::price_feed::PriceFeed; + use sui::object::{Self, UID, ID}; + use sui::tx_context::{TxContext}; + use sui::dynamic_object_field::{Self}; + use sui::table::{Self}; + use pyth::price_feed::{Self, PriceFeed}; + use pyth::price_identifier::{PriceIdentifier}; + + const KEY: vector = b"price_info"; + + friend pyth::pyth; + + /// Sui Object version of PriceInfo. + /// Has a key and lives in global store. + struct PriceInfoObject has key, store { + id: UID, + price_info: PriceInfo + } + + /// Copyable and droppable. struct PriceInfo has copy, drop, store { attestation_time: u64, arrival_time: u64, price_feed: PriceFeed, } - public fun new(attestation_time: u64, arrival_time: u64, price_feed: PriceFeed): PriceInfo { + /// Creates a table which maps a PriceIdentifier to the + /// UID (in bytes) of the corresponding Sui PriceInfoObject. + public fun new_price_info_registry(parent_id: &mut UID, ctx: &mut TxContext) { + assert!( + !dynamic_object_field::exists_(parent_id, KEY), + 0 // TODO - add custom error message + ); + dynamic_object_field::add( + parent_id, + KEY, + table::new(ctx) + ) + } + + public fun add(parent_id: &mut UID, price_identifier: PriceIdentifier, id: ID) { + assert!( + !contains(parent_id, price_identifier), + 0 // TODO - add custom error message + ); + table::add( + dynamic_object_field::borrow_mut(parent_id, KEY), + price_identifier, + id + ) + } + + public fun contains(parent_id: &UID, price_identifier: PriceIdentifier): bool { + let ref = dynamic_object_field::borrow(parent_id, KEY); + table::contains(ref, price_identifier) + } + + public fun new_price_info_object( + price_info: PriceInfo, + ctx: &mut TxContext + ): PriceInfoObject { + PriceInfoObject { + id: object::new(ctx), + price_info: price_info + } + } + + public fun new_price_info( + attestation_time: u64, + arrival_time: u64, + price_feed: PriceFeed, + ): PriceInfo { PriceInfo { attestation_time: attestation_time, arrival_time: arrival_time, @@ -15,6 +78,27 @@ module pyth::price_info { } } + #[test_only] + public fun destroy(price_info: PriceInfoObject){ + let PriceInfoObject { + id: id, + price_info: _, + } = price_info; + object::delete(id); + } + + public fun uid_to_inner(price_info: &PriceInfoObject): ID { + object::uid_to_inner(&price_info.id) + } + + public fun get_price_info_from_price_info_object(price_info: &PriceInfoObject): PriceInfo { + price_info.price_info + } + + public fun get_price_info_price_identifier(price_info: &PriceInfo): PriceIdentifier { + price_feed::get_price_identifier(&price_info.price_feed) + } + public fun get_price_feed(price_info: &PriceInfo): &PriceFeed { &price_info.price_feed } @@ -26,4 +110,11 @@ module pyth::price_info { public fun get_arrival_time(price_info: &PriceInfo): u64 { price_info.arrival_time } + + public(friend) fun update_price_info_object( + price_info_object: &mut PriceInfoObject, + price_info: PriceInfo + ) { + price_info_object.price_info = price_info; + } } diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 7f60d433f7..cfa2f67605 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -1,16 +1,28 @@ module pyth::pyth { use std::vector; use sui::tx_context::{TxContext}; + use sui::coin::{Coin}; + use sui::sui::{SUI}; + use sui::transfer::{Self}; + use sui::tx_context::{Self}; + use pyth::event::{Self as pyth_event}; use pyth::data_source::{Self, DataSource}; - //use pyth::set::{Self}; - use pyth::state::{Self}; + use pyth::state::{Self as state, State as PythState, DeployerCap}; + use pyth::price_info::{Self, PriceInfo, PriceInfoObject}; + use pyth::batch_price_attestation::{Self}; + use pyth::price_feed::{Self}; + use pyth::price::{Self}; use wormhole::external_address::{Self}; + use wormhole::vaa::{Self}; + use wormhole::state::{State as WormState}; - struct PythInitializationEvent has copy, drop {} - fun init( + /// Call init_and_share_state with deployer cap to initialize + /// state and emit event corresponding to Pyth initialization. + public entry fun init_pyth( + deployer: DeployerCap, stale_price_threshold: u64, governance_emitter_chain_id: u64, governance_emitter_address: vector, @@ -20,6 +32,7 @@ module pyth::pyth { ctx: &mut TxContext ) { state::init_and_share_state( + deployer, stale_price_threshold, update_fee, data_source::new( @@ -30,12 +43,16 @@ module pyth::pyth { data_sources_emitter_addresses, ), ctx - ) + ); + + // Emit Pyth initialization event. + pyth_event::emit_pyth_initialization_event(); } fun parse_data_sources( emitter_chain_ids: vector, - emitter_addresses: vector>): vector { + emitter_addresses: vector> + ): vector { // TODO - add custom error type error::data_source_emitter_address_and_chain_ids_different_lengths() assert!(vector::length(&emitter_chain_ids) == vector::length(&emitter_addresses), 0); @@ -53,4 +70,171 @@ module pyth::pyth { sources } + /// Create and share new price feed objects if they don't already exist. + public fun create_price_feeds( + worm_state: &mut WormState, + pyth_state: &PythState, + vaas: vector>, + ctx: &mut TxContext + ){ + while (!vector::is_empty(&vaas)) { + let vaa = vector::pop_back(&mut vaas); + + // Deserialize the VAA + let vaa = vaa::parse_and_verify(worm_state, vaa, ctx); + + // Check that the VAA is from a valid data source (emitter) + assert!( + state::is_valid_data_source( + pyth_state, + data_source::new( + (vaa::emitter_chain(&vaa) as u64), + vaa::emitter_address(&vaa)) + ), + 0); // TODO - use custom error message - error::invalid_data_source() + + // Deserialize the batch price attestation + let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), ctx)); + while (!vector::is_empty(&price_infos)){ + let cur_price_info = vector::pop_back(&mut price_infos); + + // Only create new Sui PriceInfoObject if not already + // registered with the Pyth State object. + if (!state::price_feed_object_exists( + pyth_state, + price_feed::get_price_identifier( + price_info::get_price_feed(&cur_price_info) + ) + ) + ){ + // Create and share newly created Sui PriceInfoObject containing a price feed. + let new_price_info_object = price_info::new_price_info_object(cur_price_info, ctx); + transfer::share_object(new_price_info_object); + } + } + }; + } + + /// Update PriceInfo objects and corresponding price feeds with the + /// data in the given VAAs. + /// + /// The vaas argument is a vector of VAAs encoded as bytes. + /// + /// The javascript https://github.com/pyth-network/pyth-js/tree/main/pyth-aptos-js package + /// should be used to fetch these VAAs from the Price Service. More information about this + /// process can be found at https://docs.pyth.network/consume-data. + /// + /// The given fee must contain a sufficient number of coins to pay the update fee for the given vaas. + /// The update fee amount can be queried by calling get_update_fee(&vaas). + /// + /// Please read more information about the update fee here: https://docs.pyth.network/consume-data/on-demand#fees + public fun update_price_feeds( + worm_state: &WormState, + pyth_state: &PythState, + vaas: vector>, + price_info_objects: &mut vector, + fee: Coin, + ctx: &mut TxContext + ) { + // Charge the message update fee + // TODO - error::insufficient_fee() + //assert!(get_update_fee(&vaas) <= coin::value(&fee), 0); + transfer::transfer(fee, @pyth); + + // Update the price feed from each VAA + while (!vector::is_empty(&vaas)) { + update_price_feed_from_single_vaa( + worm_state, + pyth_state, + vector::pop_back(&mut vaas), + price_info_objects, + ctx + ); + }; + } + + /// Precondition: A Sui object of type PriceInfoObject must exist for each update + /// encoded in the worm_vaa (batch_attestation_vaa). These should be passed in + /// via the price_info_objects argument. + fun update_price_feed_from_single_vaa( + worm_state: &WormState, + pyth_state: &PythState, + worm_vaa: vector, + price_info_objects: &mut vector, + ctx: &mut TxContext + ) { + // Deserialize the VAA + let vaa = vaa::parse_and_verify(worm_state, worm_vaa, ctx); + + // Check that the VAA is from a valid data source (emitter) + assert!( + state::is_valid_data_source( + pyth_state, + data_source::new( + (vaa::emitter_chain(&vaa) as u64), + vaa::emitter_address(&vaa)) + ), + 0); // TODO - use custom error message - error::invalid_data_source() + + // Deserialize the batch price attestation + let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), ctx)); + + // Update price info objects. + update_cache(price_infos, price_info_objects, ctx); + } + + /// Update PriceInfoObjects using up-to-date PriceInfos. + fun update_cache( + updates: vector, + price_info_objects: &mut vector, + ctx: &mut TxContext + ){ + while (!vector::is_empty(&updates)) { + let update = vector::pop_back(&mut updates); + let i = 0; + let found = false; + // Find PriceInfoObjects corresponding to the current update (PriceInfo). + // TODO - This for loop might be expensive if there are a large + // number of updates and/or price_info_objects we are updating. + while (i < vector::length(price_info_objects)){ + // Check if the current price info object corresponds to the price feed that + // the update is meant for. + let price_info = price_info::get_price_info_from_price_info_object(vector::borrow(price_info_objects, i)); + if (price_info::get_price_info_price_identifier(&price_info) == + price_info::get_price_info_price_identifier(&update)){ + found = true; + // TODO: use clock timestamp instead of epoch in the future + pyth_event::emit_price_feed_update(price_feed::from(price_info::get_price_feed(&update)), tx_context::epoch(ctx)); + + // Update the price info object with the new updated price info. + if (is_fresh_update(&update, vector::borrow(price_info_objects, i))){ + price_info::update_price_info_object( + vector::borrow_mut(price_info_objects, i), + update + ); + } + } + }; + if (!found){ + // TODO - throw error, since the price_feeds in price_info_objects do + // not constitute a superset of the price_feeds to be updated + } + }; + vector::destroy_empty(updates); + } + + /// Determine if the given price update is "fresh": we have nothing newer already cached for that + /// price feed within a PriceInfoObject. + fun is_fresh_update(update: &PriceInfo, price_info_object: &PriceInfoObject): bool { + // Get the timestamp of the update's current price + let price_feed = price_info::get_price_feed(update); + let update_timestamp = price::get_timestamp(&price_feed::get_price(price_feed)); + + // Get the timestamp of the cached data for the price identifier + let cached_price_info = price_info::get_price_info_from_price_info_object(price_info_object); + let cached_price_feed = price_info::get_price_feed(&cached_price_info); + let cached_timestamp = price::get_timestamp(&price_feed::get_price(cached_price_feed)); + + update_timestamp > cached_timestamp + } } diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index 7e1cd73640..637dd1e876 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -1,11 +1,12 @@ module pyth::state { use std::vector; - use sui::object::{Self, UID}; + use sui::object::{Self, UID, ID}; use sui::transfer::{Self}; use sui::tx_context::{Self, TxContext}; - use pyth::data_source::{DataSource}; - use pyth::set::{Self, Set}; + use pyth::data_source::{Self, DataSource}; + use pyth::price_info::{Self}; + use pyth::price_identifier::{PriceIdentifier}; friend pyth::pyth; @@ -17,15 +18,10 @@ module pyth::state { struct State has key { id: UID, - // TODO - Make data_sources a dynamic field of State, - // inside of something embedded in State, because there will be - // 10k+ data sources in the future, and we want to minimize the - // size of State. - data_sources: Set, governance_data_source: DataSource, last_executed_governance_sequence: u64, stale_price_threshold: u64, - base_update_fee: u64 + base_update_fee: u64, } fun init(ctx: &mut TxContext) { @@ -49,22 +45,34 @@ module pyth::state { let DeployerCap { id } = deployer; object::delete(id); - // Convert the vector of DataSource objects into a set - // of DataSource objects - let data_sources = set::new(ctx); + let uid = object::new(ctx); + + // Create a set that contains all registered data sources and + // attach it to uid as a dynamic field to minimize the + // size of State. + data_source::new_data_source_registry(&mut uid, ctx); + + // Create a table that tracks the object IDs of price feeds and + // attach it to the uid as a dynamic object field to minimize the + // size of State. + price_info::new_price_info_registry(&mut uid, ctx); + + // Iterate through data sources and add them to the data source + // registry in state. while (!vector::is_empty(&sources)) { - set::add(&mut data_sources, vector::pop_back(&mut sources)); + data_source::add(&mut uid, vector::pop_back(&mut sources)); }; - transfer::share_object( - State { - id: object::new(ctx), - data_sources, - governance_data_source, - last_executed_governance_sequence: 0, - stale_price_threshold, - base_update_fee - } - ); + + // Share state so that is a shared Sui object. + transfer::share_object( + State { + id: uid, + governance_data_source, + last_executed_governance_sequence: 0, + stale_price_threshold, + base_update_fee, + } + ); } // Accessors @@ -77,7 +85,7 @@ module pyth::state { } public fun is_valid_data_source(s: &State, data_source: DataSource): bool { - set::contains(&s.data_sources, data_source) + data_source::contains(&s.id, data_source) } public fun is_valid_governance_data_source(s: &State, source: DataSource): bool { @@ -88,14 +96,17 @@ module pyth::state { s.last_executed_governance_sequence } + public fun price_feed_object_exists(s: &State, p: PriceIdentifier): bool { + price_info::contains(&s.id, p) + } + // Setters public(friend) fun set_data_sources(s: &mut State, new_sources: vector) { - // Empty the existing set of data sources instead of dropping it, - // because it does not have drop ability. - set::empty(&mut s.data_sources); - // Add new sources to state.data_sources. + // Empty the existing table of data sources registered in state. + data_source::empty(&mut s.id); + // Add the new data sources to the dynamic field registry. while (!vector::is_empty(&new_sources)) { - set::add(&mut s.data_sources, vector::pop_back(&mut new_sources)); + data_source::add(&mut s.id, vector::pop_back(&mut new_sources)); }; } @@ -114,4 +125,8 @@ module pyth::state { public(friend) fun set_stale_price_threshold_secs(s: &mut State, threshold_secs: u64) { s.stale_price_threshold = threshold_secs; } + + public(friend) fun register_price_feed(s: &mut State, p: PriceIdentifier, id: ID){ + price_info::add(&mut s.id, p, id); + } } From 096f31ab7828b72b5cd723b306dec0b2d0840db1 Mon Sep 17 00:00:00 2001 From: optke3 Date: Thu, 23 Mar 2023 20:11:26 +0000 Subject: [PATCH 07/32] register price info object with pyth state after creation --- .../sui/contracts/sources/price_info.move | 2 +- target_chains/sui/contracts/sources/pyth.move | 16 +++++++++++----- target_chains/sui/contracts/sources/state.move | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/target_chains/sui/contracts/sources/price_info.move b/target_chains/sui/contracts/sources/price_info.move index d49695d30b..9da41d6ee2 100644 --- a/target_chains/sui/contracts/sources/price_info.move +++ b/target_chains/sui/contracts/sources/price_info.move @@ -95,7 +95,7 @@ module pyth::price_info { price_info.price_info } - public fun get_price_info_price_identifier(price_info: &PriceInfo): PriceIdentifier { + public fun get_price_identifier(price_info: &PriceInfo): PriceIdentifier { price_feed::get_price_identifier(&price_info.price_feed) } diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index cfa2f67605..d7666b407a 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -72,8 +72,8 @@ module pyth::pyth { /// Create and share new price feed objects if they don't already exist. public fun create_price_feeds( - worm_state: &mut WormState, - pyth_state: &PythState, + worm_state: &WormState, + pyth_state: &mut PythState, vaas: vector>, ctx: &mut TxContext ){ @@ -107,8 +107,14 @@ module pyth::pyth { ) ) ){ - // Create and share newly created Sui PriceInfoObject containing a price feed. + // Create and share newly created Sui PriceInfoObject containing a price feed, + // and then register a copy of its ID with State. let new_price_info_object = price_info::new_price_info_object(cur_price_info, ctx); + let price_identifier = price_info::get_price_identifier(&cur_price_info); + let id = price_info::uid_to_inner(&new_price_info_object); + + state::register_price_info_object(pyth_state, price_identifier, id); + transfer::share_object(new_price_info_object); } } @@ -200,8 +206,8 @@ module pyth::pyth { // Check if the current price info object corresponds to the price feed that // the update is meant for. let price_info = price_info::get_price_info_from_price_info_object(vector::borrow(price_info_objects, i)); - if (price_info::get_price_info_price_identifier(&price_info) == - price_info::get_price_info_price_identifier(&update)){ + if (price_info::get_price_identifier(&price_info) == + price_info::get_price_identifier(&update)){ found = true; // TODO: use clock timestamp instead of epoch in the future pyth_event::emit_price_feed_update(price_feed::from(price_info::get_price_feed(&update)), tx_context::epoch(ctx)); diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index 637dd1e876..ec6160e73a 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -110,6 +110,10 @@ module pyth::state { }; } + public(friend) fun register_price_info_object(s: &mut State, price_identifier: PriceIdentifier, id: ID) { + price_info::add(&mut s.id, price_identifier, id); + } + public(friend) fun set_last_executed_governance_sequence(s: &mut State, sequence: u64) { s.last_executed_governance_sequence = sequence; } From 690107d496cef24afc76185e16d8ceecac753c03 Mon Sep 17 00:00:00 2001 From: optke3 Date: Thu, 30 Mar 2023 18:40:34 +0000 Subject: [PATCH 08/32] sui governance --- target_chains/sui/contracts/Move.toml | 4 +- .../sources/governance/contract_upgrade.move | 12 +++ .../sources/governance/governance.move | 73 ++++++++++++++++ .../sources/governance/governance_action.move | 38 +++++++++ .../governance/governance_instruction.move | 84 +++++++++++++++++++ .../sources/governance/set_data_sources.move | 43 ++++++++++ .../set_governance_data_source.move | 36 ++++++++ .../governance/set_stale_price_threshold.move | 25 ++++++ .../sources/governance/set_update_fee.move | 40 +++++++++ target_chains/sui/contracts/sources/pyth.move | 4 +- .../sui/contracts/sources/state.move | 6 ++ 11 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 target_chains/sui/contracts/sources/governance/contract_upgrade.move create mode 100644 target_chains/sui/contracts/sources/governance/governance.move create mode 100644 target_chains/sui/contracts/sources/governance/governance_action.move create mode 100644 target_chains/sui/contracts/sources/governance/governance_instruction.move create mode 100644 target_chains/sui/contracts/sources/governance/set_data_sources.move create mode 100644 target_chains/sui/contracts/sources/governance/set_governance_data_source.move create mode 100644 target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move create mode 100644 target_chains/sui/contracts/sources/governance/set_update_fee.move diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index 7db7a62dd3..d1fc1dd52c 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -4,8 +4,8 @@ version = "0.0.1" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" -subdir = "crates/sui-framework" -rev = "157ac72030d014f17d76cefe81f3915b4afab2c9" +subdir = "crates/sui-framework/packages/sui-framework" +rev = "82c9c80c11488858f1d3930f47ec9f335a566683" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move new file mode 100644 index 0000000000..c2c010f8c6 --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -0,0 +1,12 @@ +module pyth::contract_upgrade { + use pyth::state::{State}; + + use wormhole::state::{State as WormState}; + + friend pyth::governance; + + /// Payload should be the bytes digest of the new contract. + public(friend) fun execute(_worm_state: &WormState, _pyth_state: &State, _payload: vector){ + // TODO + } +} \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move new file mode 100644 index 0000000000..40b26ad95a --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -0,0 +1,73 @@ +module pyth::governance { + use sui::tx_context::{TxContext}; + + use pyth::data_source::{Self}; + use pyth::governance_instruction; + use pyth::governance_action; + use pyth::contract_upgrade; + use pyth::set_governance_data_source; + use pyth::set_data_sources; + use pyth::set_stale_price_threshold; + use pyth::state::{State}; + use pyth::set_update_fee; + use pyth::state; + + use wormhole::vaa::{Self, VAA}; + use wormhole::state::{State as WormState}; + + public entry fun execute_governance_instruction( + pyth_state : &mut State, + worm_state: &WormState, + vaa_bytes: vector, + ctx: &mut TxContext + ) { + let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, ctx); + let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa)); + + // Dispatch the instruction to the appropiate handler + let action = governance_instruction::get_action(&instruction); + if (action == governance_action::new_contract_upgrade()) { + assert!(governance_instruction::get_target_chain_id(&instruction) != 0, + 0); // TODO - error::governance_contract_upgrade_chain_id_zero() + contract_upgrade::execute(worm_state, pyth_state, governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_governance_data_source()) { + set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_data_sources()) { + set_data_sources::execute(pyth_state, governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_update_fee()) { + set_update_fee::execute(pyth_state, governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_stale_price_threshold()) { + set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction)); + } else { + governance_instruction::destroy(instruction); + assert!(false, 0); // TODO - error::invalid_governance_action() + } + } + + fun parse_and_verify_governance_vaa( + pyth_state: &mut State, + worm_state: &WormState, + bytes: vector, + ctx: &mut TxContext + ): VAA { + let parsed_vaa = vaa::parse_and_verify(worm_state, bytes, ctx); + + // Check that the governance data source is valid + assert!( + state::is_valid_governance_data_source( + pyth_state, + data_source::new( + (vaa::emitter_chain(&parsed_vaa) as u64), + vaa::emitter_address(&parsed_vaa))), + 0); // TODO - error::invalid_governance_data_source() + + // Check that the sequence number is greater than the last executed governance VAA + let sequence = vaa::sequence(&parsed_vaa); + assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), 0); // TODO - error::invalid_governance_sequence_number() + state::set_last_executed_governance_sequence(pyth_state, sequence); + + parsed_vaa + } +} + +// TODO - add tests \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/governance/governance_action.move b/target_chains/sui/contracts/sources/governance/governance_action.move new file mode 100644 index 0000000000..0426083364 --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/governance_action.move @@ -0,0 +1,38 @@ +module pyth::governance_action { + //use pyth::error; + + const CONTRACT_UPGRADE: u8 = 0; + const SET_GOVERNANCE_DATA_SOURCE: u8 = 1; + const SET_DATA_SOURCES: u8 = 2; + const SET_UPDATE_FEE: u8 = 3; + const SET_STALE_PRICE_THRESHOLD: u8 = 4; + + struct GovernanceAction has copy, drop { + value: u8, + } + + public fun from_u8(value: u8): GovernanceAction { + assert!(CONTRACT_UPGRADE <= value && value <= SET_STALE_PRICE_THRESHOLD, 0); //TODO - add specific error: error::invalid_governance_action() + GovernanceAction { value } + } + + public fun new_contract_upgrade(): GovernanceAction { + GovernanceAction { value: CONTRACT_UPGRADE } + } + + public fun new_set_governance_data_source(): GovernanceAction { + GovernanceAction { value: SET_GOVERNANCE_DATA_SOURCE } + } + + public fun new_set_data_sources(): GovernanceAction { + GovernanceAction { value: SET_DATA_SOURCES } + } + + public fun new_set_update_fee(): GovernanceAction { + GovernanceAction { value: SET_UPDATE_FEE } + } + + public fun new_set_stale_price_threshold(): GovernanceAction { + GovernanceAction { value: SET_STALE_PRICE_THRESHOLD } + } +} diff --git a/target_chains/sui/contracts/sources/governance/governance_instruction.move b/target_chains/sui/contracts/sources/governance/governance_instruction.move new file mode 100644 index 0000000000..841e28addd --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/governance_instruction.move @@ -0,0 +1,84 @@ +module pyth::governance_instruction { + use wormhole::cursor; + use pyth::deserialize; + use pyth::governance_action::{Self, GovernanceAction}; + + const MAGIC: vector = x"5054474d"; // "PTGM": Pyth Governance Message + const MODULE: u8 = 1; + + struct GovernanceInstruction { + module_: u8, + action: GovernanceAction, + target_chain_id: u64, + payload: vector, + } + + fun validate(instruction: &GovernanceInstruction) { + assert!(instruction.module_ == MODULE, 0); // TODO - add custom error::invalid_governance_module() + let target_chain_id = instruction.target_chain_id; + assert!(target_chain_id == (wormhole::state::chain_id() as u64) || target_chain_id == 0, 0); // TODO - custom error: error::invalid_governance_target_chain_id() + } + + public fun from_byte_vec(bytes: vector): GovernanceInstruction { + let cursor = cursor::new(bytes); + let magic = deserialize::deserialize_vector(&mut cursor, 4); + assert!(magic == MAGIC, 0); // TODO error::invalid_governance_magic_value() + let module_ = deserialize::deserialize_u8(&mut cursor); + let action = governance_action::from_u8(deserialize::deserialize_u8(&mut cursor)); + let target_chain_id = deserialize::deserialize_u16(&mut cursor); + let payload = cursor::take_rest(cursor); + + let instruction = GovernanceInstruction { + module_, + action, + target_chain_id : (target_chain_id as u64), + payload + }; + validate(&instruction); + + instruction + } + + public fun get_module(instruction: &GovernanceInstruction): u8 { + instruction.module_ + } + + public fun get_action(instruction: &GovernanceInstruction): GovernanceAction { + instruction.action + } + + public fun get_target_chain_id(instruction: &GovernanceInstruction): u64 { + instruction.target_chain_id + } + + public fun destroy(instruction: GovernanceInstruction): vector { + let GovernanceInstruction { + module_: _, + action: _, + target_chain_id: _, + payload: payload + } = instruction; + payload + } + + #[test] + #[expected_failure] + fun test_from_byte_vec_invalid_magic() { + let bytes = x"5054474eb01087a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } + + #[test] + #[expected_failure] + fun test_from_byte_vec_invalid_module() { + let bytes = x"5054474db00187a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } + + #[test] + #[expected_failure] + fun test_from_byte_vec_invalid_target_chain_id() { + let bytes = x"5054474db00187a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } +} diff --git a/target_chains/sui/contracts/sources/governance/set_data_sources.move b/target_chains/sui/contracts/sources/governance/set_data_sources.move new file mode 100644 index 0000000000..b1d442592b --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/set_data_sources.move @@ -0,0 +1,43 @@ +module pyth::set_data_sources { + use std::vector; + + use wormhole::cursor; + use wormhole::external_address::{Self}; + + use pyth::deserialize; + use pyth::data_source::{Self, DataSource}; + use pyth::state::{Self, State}; + + friend pyth::governance; + + struct SetDataSources { + sources: vector, + } + + public(friend) fun execute(state: &mut State, payload: vector) { + let SetDataSources { sources } = from_byte_vec(payload); + state::set_data_sources(state, sources); + } + + fun from_byte_vec(bytes: vector): SetDataSources { + let cursor = cursor::new(bytes); + let data_sources_count = deserialize::deserialize_u8(&mut cursor); + + let sources = vector::empty(); + + let i = 0; + while (i < data_sources_count) { + let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); + let emitter_address = external_address::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)); + vector::push_back(&mut sources, data_source::new((emitter_chain_id as u64), emitter_address)); + + i = i + 1; + }; + + cursor::destroy_empty(cursor); + + SetDataSources { + sources + } + } +} diff --git a/target_chains/sui/contracts/sources/governance/set_governance_data_source.move b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move new file mode 100644 index 0000000000..77328a8788 --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move @@ -0,0 +1,36 @@ +module pyth::set_governance_data_source { + use pyth::deserialize; + use pyth::data_source; + use pyth::state::{Self, State}; + + use wormhole::cursor; + use wormhole::external_address::{Self, ExternalAddress}; + //use wormhole::state::{Self} + + friend pyth::governance; + + struct SetGovernanceDataSource { + emitter_chain_id: u64, + emitter_address: ExternalAddress, + initial_sequence: u64, + } + + public(friend) fun execute(pyth_state: &mut State, payload: vector) { + let SetGovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence } = from_byte_vec(payload); + state::set_governance_data_source(pyth_state, data_source::new(emitter_chain_id, emitter_address)); + state::set_last_executed_governance_sequence(pyth_state, initial_sequence); + } + + fun from_byte_vec(bytes: vector): SetGovernanceDataSource { + let cursor = cursor::new(bytes); + let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); + let emitter_address = external_address::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)); + let initial_sequence = deserialize::deserialize_u64(&mut cursor); + cursor::destroy_empty(cursor); + SetGovernanceDataSource { + emitter_chain_id: (emitter_chain_id as u64), + emitter_address, + initial_sequence + } + } +} diff --git a/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move b/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move new file mode 100644 index 0000000000..6fdb6a2ed0 --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move @@ -0,0 +1,25 @@ +module pyth::set_stale_price_threshold { + use wormhole::cursor; + use pyth::deserialize; + use pyth::state::{Self, State}; + + friend pyth::governance; + + struct SetStalePriceThreshold { + threshold: u64, + } + + public(friend) fun execute(state: &mut State, payload: vector) { + let SetStalePriceThreshold { threshold } = from_byte_vec(payload); + state::set_stale_price_threshold_secs(state, threshold); + } + + fun from_byte_vec(bytes: vector): SetStalePriceThreshold { + let cursor = cursor::new(bytes); + let threshold = deserialize::deserialize_u64(&mut cursor); + cursor::destroy_empty(cursor); + SetStalePriceThreshold { + threshold + } + } +} diff --git a/target_chains/sui/contracts/sources/governance/set_update_fee.move b/target_chains/sui/contracts/sources/governance/set_update_fee.move new file mode 100644 index 0000000000..c300a69855 --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/set_update_fee.move @@ -0,0 +1,40 @@ +module pyth::set_update_fee { + use sui::math::{Self}; + + use pyth::deserialize; + use pyth::state::{Self, State}; + + use wormhole::cursor; + + + friend pyth::governance; + + const MAX_U64: u128 = (1 << 64) - 1; + + struct SetUpdateFee { + mantissa: u64, + exponent: u64, + } + + public(friend) fun execute(pyth_state: &mut State, payload: vector) { + let SetUpdateFee { mantissa, exponent } = from_byte_vec(payload); + assert!(exponent <= 255, 0); // TODO - throw error that exponent does not fit in a u8 + let fee = apply_exponent(mantissa, (exponent as u8)); + state::set_base_update_fee(pyth_state, fee); + } + + fun from_byte_vec(bytes: vector): SetUpdateFee { + let cursor = cursor::new(bytes); + let mantissa = deserialize::deserialize_u64(&mut cursor); + let exponent = deserialize::deserialize_u64(&mut cursor); + cursor::destroy_empty(cursor); + SetUpdateFee { + mantissa, + exponent, + } + } + + fun apply_exponent(mantissa: u64, exponent: u8): u64 { + mantissa * math::pow(10, exponent) + } +} diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index d7666b407a..c368bbc1ae 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -115,7 +115,7 @@ module pyth::pyth { state::register_price_info_object(pyth_state, price_identifier, id); - transfer::share_object(new_price_info_object); + transfer::public_share_object(new_price_info_object); } } }; @@ -145,7 +145,7 @@ module pyth::pyth { // Charge the message update fee // TODO - error::insufficient_fee() //assert!(get_update_fee(&vaas) <= coin::value(&fee), 0); - transfer::transfer(fee, @pyth); + transfer::public_transfer(fee, @pyth); // Update the price feed from each VAA while (!vector::is_empty(&vaas)) { diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index ec6160e73a..dca62a116e 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -9,6 +9,12 @@ module pyth::state { use pyth::price_identifier::{PriceIdentifier}; friend pyth::pyth; + friend pyth::governance_action; + friend pyth::set_update_fee; + friend pyth::set_stale_price_threshold; + friend pyth::set_data_sources; + friend pyth::governance; + friend pyth::set_governance_data_source; /// Capability for creating a bridge state object, granted to sender when this /// module is deployed From 7966b572e69f1ed94fa1fdf224de1c7d318e34ed Mon Sep 17 00:00:00 2001 From: optke3 Date: Thu, 30 Mar 2023 18:48:14 +0000 Subject: [PATCH 09/32] some newlines --- .../sui/contracts/sources/governance/contract_upgrade.move | 2 +- target_chains/sui/contracts/sources/governance/governance.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move index c2c010f8c6..e0130c1304 100644 --- a/target_chains/sui/contracts/sources/governance/contract_upgrade.move +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -9,4 +9,4 @@ module pyth::contract_upgrade { public(friend) fun execute(_worm_state: &WormState, _pyth_state: &State, _payload: vector){ // TODO } -} \ No newline at end of file +} diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move index 40b26ad95a..bdebad43e3 100644 --- a/target_chains/sui/contracts/sources/governance/governance.move +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -70,4 +70,4 @@ module pyth::governance { } } -// TODO - add tests \ No newline at end of file +// TODO - add tests From e0fc80322623a7768c155b12094240f1e89500e4 Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 3 Apr 2023 21:01:46 +0000 Subject: [PATCH 10/32] error codes --- .../sources/batch_price_attestation.move | 58 ++++++++++--------- .../sui/contracts/sources/data_source.move | 6 +- .../sui/contracts/sources/event.move | 2 +- .../sources/governance/governance.move | 13 +++-- .../sources/governance/governance_action.move | 4 +- .../governance/governance_instruction.move | 9 ++- .../sources/governance/set_update_fee.move | 3 +- .../sui/contracts/sources/price_info.move | 6 +- target_chains/sui/contracts/sources/pyth.move | 45 +++++++++----- 9 files changed, 92 insertions(+), 54 deletions(-) diff --git a/target_chains/sui/contracts/sources/batch_price_attestation.move b/target_chains/sui/contracts/sources/batch_price_attestation.move index f7ccd00ae7..fd4ada51da 100644 --- a/target_chains/sui/contracts/sources/batch_price_attestation.move +++ b/target_chains/sui/contracts/sources/batch_price_attestation.move @@ -1,28 +1,24 @@ module pyth::batch_price_attestation { - - use sui::tx_context::{Self, TxContext}; + use std::vector::{Self}; + use sui::clock::{Self, Clock}; use pyth::price_feed::{Self}; use pyth::price_info::{Self, PriceInfo}; use pyth::price_identifier::{Self}; use pyth::price_status; use pyth::deserialize::{Self}; - // TODO - Import Sui clock and use it for timekeeping instead of tx_context::epoch. - // Replace epoch in deserialize_price_info with sui clock timestamp, and usage - // of epoch in test_deserialize_batch_price_attestation. - // TODO - Use specific error messages in this module, specifically - // for invalid_attestation_magic_value and invalid_batch_attestation_header_size. + use wormhole::cursor::{Self, Cursor}; use wormhole::bytes::{Self}; - use std::vector::{Self}; - #[test_only] use pyth::price; #[test_only] use pyth::i64; const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes + const E_INVALID_ATTESTATION_MAGIC_VALUE: u64 = 0; + const E_INVALID_BATCH_ATTESTATION_HEADER_SIZE: u64 = 1; struct BatchPriceAttestation { header: Header, @@ -41,13 +37,13 @@ module pyth::batch_price_attestation { fun deserialize_header(cur: &mut Cursor): Header { let magic = (deserialize::deserialize_u32(cur) as u64); - assert!(magic == MAGIC, 0); // TODO - add specific error value - error::invalid_attestation_magic_value() + assert!(magic == MAGIC, E_INVALID_ATTESTATION_MAGIC_VALUE); let version_major = deserialize::deserialize_u16(cur); let version_minor = deserialize::deserialize_u16(cur); let header_size = deserialize::deserialize_u16(cur); let payload_id = deserialize::deserialize_u8(cur); - assert!(header_size >= 1, 0); // TODO - add specific error value - error::invalid_batch_attestation_header_size() + assert!(header_size >= 1, E_INVALID_BATCH_ATTESTATION_HEADER_SIZE); let unknown_header_bytes = header_size - 1; let _unknown = bytes::take_bytes(cur, (unknown_header_bytes as u64)); @@ -84,7 +80,7 @@ module pyth::batch_price_attestation { vector::borrow(&batch.price_infos, index) } - public fun deserialize(bytes: vector, ctx: &mut TxContext): BatchPriceAttestation { + public fun deserialize(bytes: vector, clock: &Clock): BatchPriceAttestation { let cur = cursor::new(bytes); let header = deserialize_header(&mut cur); @@ -94,7 +90,7 @@ module pyth::batch_price_attestation { let i = 0; while (i < attestation_count) { - let price_info = deserialize_price_info(&mut cur, ctx); + let price_info = deserialize_price_info(&mut cur, clock); vector::push_back(&mut price_infos, price_info); // Consume any excess bytes @@ -113,7 +109,7 @@ module pyth::batch_price_attestation { } } - fun deserialize_price_info(cur: &mut Cursor, ctx: &mut TxContext): PriceInfo { + fun deserialize_price_info(cur: &mut Cursor, clock: &Clock): PriceInfo { // Skip obselete field let _product_identifier = deserialize::deserialize_vector(cur, 32); @@ -155,7 +151,7 @@ module pyth::batch_price_attestation { price_info::new_price_info( attestation_time, - tx_context::epoch(ctx), //TODO - use Sui Clock to get timestamp in seconds + clock::timestamp_ms(clock) / 1000, // Divide by 1000 to get timestamp in seconds price_feed::new( price_identifier, current_price, @@ -167,21 +163,30 @@ module pyth::batch_price_attestation { #[test] #[expected_failure] fun test_deserialize_batch_price_attestation_invalid_magic() { - use sui::test_scenario::{Self, ctx}; + use sui::test_scenario::{Self, take_shared, return_shared, ctx}; let test = test_scenario::begin(@0x1234); + clock::create_for_testing(ctx(&mut test)); + test_scenario::next_tx(&mut test, @0x1234); + let test_clock = take_shared(&test); // A batch price attestation with a magic number of 0x50325749 let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; - let _ = destroy(deserialize(bytes, ctx(&mut test))); + let _ = destroy(deserialize(bytes, &test_clock)); + return_shared(test_clock); test_scenario::end(test); } #[test] fun test_deserialize_batch_price_attestation() { - use sui::test_scenario::{Self, ctx}; + use sui::test_scenario::{Self, take_shared, return_shared, ctx}; // Set the arrival time let test = test_scenario::begin(@0x1234); - let arrival_time = tx_context::epoch(ctx(&mut test)); + clock::create_for_testing(ctx(&mut test)); + test_scenario::next_tx(&mut test, @0x1234); + let test_clock = take_shared(&test); + let arrival_time_in_seconds = clock::timestamp_ms(&test_clock) / 1000; + + // let arrival_time = tx_context::epoch(ctx(&mut test)); // A raw batch price attestation // The first attestation has a status of UNKNOWN @@ -200,7 +205,7 @@ module pyth::batch_price_attestation { price_infos: vector[ price_info::new_price_info( 1663680747, - arrival_time, + arrival_time_in_seconds, price_feed::new( price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"), price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740), @@ -208,7 +213,7 @@ module pyth::batch_price_attestation { ) ), price_info::new_price_info( 1663680747, - arrival_time, + arrival_time_in_seconds, price_feed::new( price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"), price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745), @@ -216,7 +221,7 @@ module pyth::batch_price_attestation { ) ), price_info::new_price_info( 1663680747, - arrival_time, + arrival_time_in_seconds, price_feed::new( price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"), price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745), @@ -224,21 +229,22 @@ module pyth::batch_price_attestation { ) ), price_info::new_price_info( 1663680747, - arrival_time, + arrival_time_in_seconds, price_feed::new( price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"), price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745), price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745), - ) ), + ) + ), ], }; - let deserialized = deserialize(bytes, ctx(&mut test)); + let deserialized = deserialize(bytes, &test_clock); assert!(&expected == &deserialized, 1); destroy(expected); destroy(deserialized); - + return_shared(test_clock); test_scenario::end(test); } } diff --git a/target_chains/sui/contracts/sources/data_source.move b/target_chains/sui/contracts/sources/data_source.move index c8b7af8266..88b751ccd8 100644 --- a/target_chains/sui/contracts/sources/data_source.move +++ b/target_chains/sui/contracts/sources/data_source.move @@ -8,6 +8,8 @@ module pyth::data_source { use wormhole::external_address::ExternalAddress; const KEY: vector = b"data_sources"; + const E_DATA_SOURCE_REGISTRY_ALREADY_EXISTS: u64 = 0; + const E_DATA_SOURCE_ALREADY_REGISTERED: u64 = 1; struct DataSource has copy, drop, store { emitter_chain: u64, @@ -17,7 +19,7 @@ module pyth::data_source { public fun new_data_source_registry(parent_id: &mut UID, ctx: &mut TxContext) { assert!( !dynamic_field::exists_(parent_id, KEY), - 0 // TODO - add custom error type + E_DATA_SOURCE_REGISTRY_ALREADY_EXISTS // TODO - add custom error type ); dynamic_field::add( parent_id, @@ -29,7 +31,7 @@ module pyth::data_source { public fun add(parent_id: &mut UID, data_source: DataSource) { assert!( !contains(parent_id, data_source), - 0 // TODO - add custom error message + E_DATA_SOURCE_ALREADY_REGISTERED ); set::add( dynamic_field::borrow_mut(parent_id, KEY), diff --git a/target_chains/sui/contracts/sources/event.move b/target_chains/sui/contracts/sources/event.move index a57fed66ed..a40c5d218b 100644 --- a/target_chains/sui/contracts/sources/event.move +++ b/target_chains/sui/contracts/sources/event.move @@ -15,7 +15,7 @@ module pyth::event { timestamp: u64, } - public(friend) fun emit_price_feed_update(price_feed: PriceFeed, timestamp: u64) { + public(friend) fun emit_price_feed_update(price_feed: PriceFeed, timestamp: u64 /* in seconds */) { event::emit( PriceFeedUpdateEvent { price_feed, diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move index bdebad43e3..40eb979b56 100644 --- a/target_chains/sui/contracts/sources/governance/governance.move +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -15,6 +15,11 @@ module pyth::governance { use wormhole::vaa::{Self, VAA}; use wormhole::state::{State as WormState}; + const E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO: u64 = 0; + const E_INVALID_GOVERNANCE_ACTION: u64 = 1; + const E_INVALID_GOVERNANCE_DATA_SOURCE: u64 = 2; + const E_INVALID_GOVERNANCE_SEQUENCE_NUMBER: u64 = 3; + public entry fun execute_governance_instruction( pyth_state : &mut State, worm_state: &WormState, @@ -28,7 +33,7 @@ module pyth::governance { let action = governance_instruction::get_action(&instruction); if (action == governance_action::new_contract_upgrade()) { assert!(governance_instruction::get_target_chain_id(&instruction) != 0, - 0); // TODO - error::governance_contract_upgrade_chain_id_zero() + E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO); contract_upgrade::execute(worm_state, pyth_state, governance_instruction::destroy(instruction)); } else if (action == governance_action::new_set_governance_data_source()) { set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction)); @@ -40,7 +45,7 @@ module pyth::governance { set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction)); } else { governance_instruction::destroy(instruction); - assert!(false, 0); // TODO - error::invalid_governance_action() + assert!(false, E_INVALID_GOVERNANCE_ACTION); } } @@ -59,11 +64,11 @@ module pyth::governance { data_source::new( (vaa::emitter_chain(&parsed_vaa) as u64), vaa::emitter_address(&parsed_vaa))), - 0); // TODO - error::invalid_governance_data_source() + E_INVALID_GOVERNANCE_DATA_SOURCE); // Check that the sequence number is greater than the last executed governance VAA let sequence = vaa::sequence(&parsed_vaa); - assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), 0); // TODO - error::invalid_governance_sequence_number() + assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), E_INVALID_GOVERNANCE_SEQUENCE_NUMBER); state::set_last_executed_governance_sequence(pyth_state, sequence); parsed_vaa diff --git a/target_chains/sui/contracts/sources/governance/governance_action.move b/target_chains/sui/contracts/sources/governance/governance_action.move index 0426083364..efd79735ce 100644 --- a/target_chains/sui/contracts/sources/governance/governance_action.move +++ b/target_chains/sui/contracts/sources/governance/governance_action.move @@ -7,12 +7,14 @@ module pyth::governance_action { const SET_UPDATE_FEE: u8 = 3; const SET_STALE_PRICE_THRESHOLD: u8 = 4; + const E_INVALID_GOVERNANCE_ACTION: u64 = 5; + struct GovernanceAction has copy, drop { value: u8, } public fun from_u8(value: u8): GovernanceAction { - assert!(CONTRACT_UPGRADE <= value && value <= SET_STALE_PRICE_THRESHOLD, 0); //TODO - add specific error: error::invalid_governance_action() + assert!(CONTRACT_UPGRADE <= value && value <= SET_STALE_PRICE_THRESHOLD, E_INVALID_GOVERNANCE_ACTION); GovernanceAction { value } } diff --git a/target_chains/sui/contracts/sources/governance/governance_instruction.move b/target_chains/sui/contracts/sources/governance/governance_instruction.move index 841e28addd..52a3516be2 100644 --- a/target_chains/sui/contracts/sources/governance/governance_instruction.move +++ b/target_chains/sui/contracts/sources/governance/governance_instruction.move @@ -6,6 +6,9 @@ module pyth::governance_instruction { const MAGIC: vector = x"5054474d"; // "PTGM": Pyth Governance Message const MODULE: u8 = 1; + const E_INVALID_GOVERNANCE_MODULE: u64 = 0; + const E_INVALID_GOVERNANCE_MAGIC_VALUE: u64 = 1; + struct GovernanceInstruction { module_: u8, action: GovernanceAction, @@ -14,15 +17,15 @@ module pyth::governance_instruction { } fun validate(instruction: &GovernanceInstruction) { - assert!(instruction.module_ == MODULE, 0); // TODO - add custom error::invalid_governance_module() + assert!(instruction.module_ == MODULE, E_INVALID_GOVERNANCE_MODULE); let target_chain_id = instruction.target_chain_id; - assert!(target_chain_id == (wormhole::state::chain_id() as u64) || target_chain_id == 0, 0); // TODO - custom error: error::invalid_governance_target_chain_id() + assert!(target_chain_id == (wormhole::state::chain_id() as u64) || target_chain_id == 0, E_INVALID_GOVERNANCE_MODULE); } public fun from_byte_vec(bytes: vector): GovernanceInstruction { let cursor = cursor::new(bytes); let magic = deserialize::deserialize_vector(&mut cursor, 4); - assert!(magic == MAGIC, 0); // TODO error::invalid_governance_magic_value() + assert!(magic == MAGIC, E_INVALID_GOVERNANCE_MAGIC_VALUE); let module_ = deserialize::deserialize_u8(&mut cursor); let action = governance_action::from_u8(deserialize::deserialize_u8(&mut cursor)); let target_chain_id = deserialize::deserialize_u16(&mut cursor); diff --git a/target_chains/sui/contracts/sources/governance/set_update_fee.move b/target_chains/sui/contracts/sources/governance/set_update_fee.move index c300a69855..0286fc07d2 100644 --- a/target_chains/sui/contracts/sources/governance/set_update_fee.move +++ b/target_chains/sui/contracts/sources/governance/set_update_fee.move @@ -10,6 +10,7 @@ module pyth::set_update_fee { friend pyth::governance; const MAX_U64: u128 = (1 << 64) - 1; + const E_EXPONENT_DOES_NOT_FIT_IN_U8: u64 = 0; struct SetUpdateFee { mantissa: u64, @@ -18,7 +19,7 @@ module pyth::set_update_fee { public(friend) fun execute(pyth_state: &mut State, payload: vector) { let SetUpdateFee { mantissa, exponent } = from_byte_vec(payload); - assert!(exponent <= 255, 0); // TODO - throw error that exponent does not fit in a u8 + assert!(exponent <= 255, E_EXPONENT_DOES_NOT_FIT_IN_U8); let fee = apply_exponent(mantissa, (exponent as u8)); state::set_base_update_fee(pyth_state, fee); } diff --git a/target_chains/sui/contracts/sources/price_info.move b/target_chains/sui/contracts/sources/price_info.move index 9da41d6ee2..cd98aaefc3 100644 --- a/target_chains/sui/contracts/sources/price_info.move +++ b/target_chains/sui/contracts/sources/price_info.move @@ -8,6 +8,8 @@ module pyth::price_info { use pyth::price_identifier::{PriceIdentifier}; const KEY: vector = b"price_info"; + const E_PRICE_INFO_REGISTRY_ALREADY_EXISTS: u64 = 0; + const E_PRICE_IDENTIFIER_ALREADY_REGISTERED: u64 = 1; friend pyth::pyth; @@ -30,7 +32,7 @@ module pyth::price_info { public fun new_price_info_registry(parent_id: &mut UID, ctx: &mut TxContext) { assert!( !dynamic_object_field::exists_(parent_id, KEY), - 0 // TODO - add custom error message + E_PRICE_INFO_REGISTRY_ALREADY_EXISTS ); dynamic_object_field::add( parent_id, @@ -42,7 +44,7 @@ module pyth::price_info { public fun add(parent_id: &mut UID, price_identifier: PriceIdentifier, id: ID) { assert!( !contains(parent_id, price_identifier), - 0 // TODO - add custom error message + E_PRICE_IDENTIFIER_ALREADY_REGISTERED ); table::add( dynamic_object_field::borrow_mut(parent_id, KEY), diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index c368bbc1ae..0422503824 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -1,10 +1,10 @@ module pyth::pyth { use std::vector; use sui::tx_context::{TxContext}; - use sui::coin::{Coin}; + use sui::coin::{Self, Coin}; use sui::sui::{SUI}; use sui::transfer::{Self}; - use sui::tx_context::{Self}; + use sui::clock::{Self, Clock}; use pyth::event::{Self as pyth_event}; use pyth::data_source::{Self, DataSource}; @@ -18,6 +18,9 @@ module pyth::pyth { use wormhole::vaa::{Self}; use wormhole::state::{State as WormState}; + const E_DATA_SOURCE_EMITTER_ADDRESS_AND_CHAIN_IDS_DIFFERENT_LENGTHS: u64 = 0; + const E_INVALID_DATA_SOURCE: u64 = 1; + const E_INSUFFICIENT_FEE: u64 = 2; /// Call init_and_share_state with deployer cap to initialize /// state and emit event corresponding to Pyth initialization. @@ -54,8 +57,8 @@ module pyth::pyth { emitter_addresses: vector> ): vector { - // TODO - add custom error type error::data_source_emitter_address_and_chain_ids_different_lengths() - assert!(vector::length(&emitter_chain_ids) == vector::length(&emitter_addresses), 0); + assert!(vector::length(&emitter_chain_ids) == vector::length(&emitter_addresses), + E_DATA_SOURCE_EMITTER_ADDRESS_AND_CHAIN_IDS_DIFFERENT_LENGTHS); let sources = vector::empty(); let i = 0; @@ -75,6 +78,7 @@ module pyth::pyth { worm_state: &WormState, pyth_state: &mut PythState, vaas: vector>, + clock: &Clock, ctx: &mut TxContext ){ while (!vector::is_empty(&vaas)) { @@ -91,10 +95,10 @@ module pyth::pyth { (vaa::emitter_chain(&vaa) as u64), vaa::emitter_address(&vaa)) ), - 0); // TODO - use custom error message - error::invalid_data_source() + E_INVALID_DATA_SOURCE); // Deserialize the batch price attestation - let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), ctx)); + let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), clock)); while (!vector::is_empty(&price_infos)){ let cur_price_info = vector::pop_back(&mut price_infos); @@ -140,11 +144,13 @@ module pyth::pyth { vaas: vector>, price_info_objects: &mut vector, fee: Coin, + clock: &Clock, ctx: &mut TxContext ) { // Charge the message update fee - // TODO - error::insufficient_fee() - //assert!(get_update_fee(&vaas) <= coin::value(&fee), 0); + assert!(get_total_update_fee(pyth_state, &vaas) <= coin::value(&fee), E_INSUFFICIENT_FEE); + + // TODO: use Wormhole fee collector instead of transferring funds to deployer address. transfer::public_transfer(fee, @pyth); // Update the price feed from each VAA @@ -154,6 +160,7 @@ module pyth::pyth { pyth_state, vector::pop_back(&mut vaas), price_info_objects, + clock, ctx ); }; @@ -167,6 +174,7 @@ module pyth::pyth { pyth_state: &PythState, worm_vaa: vector, price_info_objects: &mut vector, + clock: &Clock, ctx: &mut TxContext ) { // Deserialize the VAA @@ -180,20 +188,20 @@ module pyth::pyth { (vaa::emitter_chain(&vaa) as u64), vaa::emitter_address(&vaa)) ), - 0); // TODO - use custom error message - error::invalid_data_source() + E_INVALID_DATA_SOURCE); // Deserialize the batch price attestation - let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), ctx)); + let price_infos = batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::take_payload(vaa), clock)); // Update price info objects. - update_cache(price_infos, price_info_objects, ctx); + update_cache(price_infos, price_info_objects, clock); } /// Update PriceInfoObjects using up-to-date PriceInfos. fun update_cache( updates: vector, price_info_objects: &mut vector, - ctx: &mut TxContext + clock: &Clock, ){ while (!vector::is_empty(&updates)) { let update = vector::pop_back(&mut updates); @@ -209,8 +217,7 @@ module pyth::pyth { if (price_info::get_price_identifier(&price_info) == price_info::get_price_identifier(&update)){ found = true; - // TODO: use clock timestamp instead of epoch in the future - pyth_event::emit_price_feed_update(price_feed::from(price_info::get_price_feed(&update)), tx_context::epoch(ctx)); + pyth_event::emit_price_feed_update(price_feed::from(price_info::get_price_feed(&update)), clock::timestamp_ms(clock)/1000); // Update the price info object with the new updated price info. if (is_fresh_update(&update, vector::borrow(price_info_objects, i))){ @@ -243,4 +250,14 @@ module pyth::pyth { update_timestamp > cached_timestamp } + + /// Get the number of AptosCoin's required to perform the given price updates. + /// + /// Please read more information about the update fee here: https://docs.pyth.network/consume-data/on-demand#fees + public fun get_total_update_fee(pyth_state: &PythState, update_data: &vector>): u64 { + state::get_base_update_fee(pyth_state) * vector::length(update_data) + } } + +// TODO - pyth tests +// https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/aptos/contracts/sources/pyth.move#L384 From 84ec0d37374cb163f4bfe124aaa72e9e7f07cb2e Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 3 Apr 2023 21:18:53 +0000 Subject: [PATCH 11/32] update and comment --- .../contracts/sources/governance/governance_instruction.move | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/target_chains/sui/contracts/sources/governance/governance_instruction.move b/target_chains/sui/contracts/sources/governance/governance_instruction.move index 52a3516be2..21fbec12d4 100644 --- a/target_chains/sui/contracts/sources/governance/governance_instruction.move +++ b/target_chains/sui/contracts/sources/governance/governance_instruction.move @@ -8,6 +8,7 @@ module pyth::governance_instruction { const E_INVALID_GOVERNANCE_MODULE: u64 = 0; const E_INVALID_GOVERNANCE_MAGIC_VALUE: u64 = 1; + const E_TARGET_CHAIN_MISMATCH: u64 = 2; struct GovernanceInstruction { module_: u8, @@ -19,13 +20,14 @@ module pyth::governance_instruction { fun validate(instruction: &GovernanceInstruction) { assert!(instruction.module_ == MODULE, E_INVALID_GOVERNANCE_MODULE); let target_chain_id = instruction.target_chain_id; - assert!(target_chain_id == (wormhole::state::chain_id() as u64) || target_chain_id == 0, E_INVALID_GOVERNANCE_MODULE); + assert!(target_chain_id == (wormhole::state::chain_id() as u64) || target_chain_id == 0, E_TARGET_CHAIN_MISMATCH); } public fun from_byte_vec(bytes: vector): GovernanceInstruction { let cursor = cursor::new(bytes); let magic = deserialize::deserialize_vector(&mut cursor, 4); assert!(magic == MAGIC, E_INVALID_GOVERNANCE_MAGIC_VALUE); + // "module" is a reserved keyword, so we use "module_" instead. let module_ = deserialize::deserialize_u8(&mut cursor); let action = governance_action::from_u8(deserialize::deserialize_u8(&mut cursor)); let target_chain_id = deserialize::deserialize_u16(&mut cursor); From c13c23d7a1e82a95f5419dfa15673822198fc1fd Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 4 Apr 2023 02:50:48 +0000 Subject: [PATCH 12/32] unit tests for pyth.move, add UpgradeCap to Pyth State (will be used for contract upgrades) --- target_chains/sui/contracts/sources/pyth.move | 155 ++++++++++++++++++ .../sui/contracts/sources/state.move | 17 ++ 2 files changed, 172 insertions(+) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 0422503824..df94e27cfb 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -5,6 +5,7 @@ module pyth::pyth { use sui::sui::{SUI}; use sui::transfer::{Self}; use sui::clock::{Self, Clock}; + use sui::package::{UpgradeCap}; use pyth::event::{Self as pyth_event}; use pyth::data_source::{Self, DataSource}; @@ -26,6 +27,7 @@ module pyth::pyth { /// state and emit event corresponding to Pyth initialization. public entry fun init_pyth( deployer: DeployerCap, + upgrade_cap: UpgradeCap, stale_price_threshold: u64, governance_emitter_chain_id: u64, governance_emitter_address: vector, @@ -36,6 +38,7 @@ module pyth::pyth { ) { state::init_and_share_state( deployer, + upgrade_cap, stale_price_threshold, update_fee, data_source::new( @@ -261,3 +264,155 @@ module pyth::pyth { // TODO - pyth tests // https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/aptos/contracts/sources/pyth.move#L384 + +module pyth::pyth_tests{ + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::clock::{Self}; + use sui::test_scenario::{Self, Scenario, ctx}; + use sui::package::Self; + use sui::object::Self; + + use pyth::state::{Self}; + use pyth::price_identifier::{Self}; + use pyth::price_info::{Self, PriceInfo}; + use pyth::price_feed::{Self}; + use pyth::data_source::{Self, DataSource}; + use pyth::i64::{Self}; + use pyth::price::{Self}; + + use wormhole::setup::{Self as wormhole_setup, DeployerCap}; + use wormhole::external_address::{Self}; + + #[test_only] + /// Init Wormhole core bridge state. + /// Init Pyth state. + /// Set initial Sui clock time, + /// Mint some SUI fee coins. + fun setup_test( + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + data_sources: vector, + base_update_fee: u64, + to_mint: u64 + ): (Scenario, Coin) { + + let deployer = @0x1234; + let scenario = test_scenario::begin(deployer); + + // Initialize Wormhole core bridge. + wormhole_setup::init_test_only(ctx(&mut scenario)); + + // Take the `DeployerCap` from the sender of the transaction. + let deployer_cap = + test_scenario::take_from_address( + &scenario, + deployer + ); + + // This will be created and sent to the transaction sender automatically + // when the contract is published. This exists in place of grabbing + // it from the sender. + let upgrade_cap = + package::test_publish( + object::id_from_address(@0x0), + test_scenario::ctx(&mut scenario) + ); + + let governance_chain = 1234; + let governance_contract = + x"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + let initial_guardians = + vector[ + x"1337133713371337133713371337133713371337", + x"c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de", + x"ba5edba5edba5edba5edba5edba5edba5edba5ed" + ]; + let guardian_set_seconds_to_live = 5678; + let message_fee = 350; + + wormhole_setup::init_and_share_state( + deployer_cap, + upgrade_cap, + governance_chain, + governance_contract, + initial_guardians, + guardian_set_seconds_to_live, + message_fee, + test_scenario::ctx(&mut scenario) + ); + + // Create and share a global clock object for timekeeping. + clock::create_for_testing(ctx(&mut scenario)); + + // Initialize Pyth state. + let pyth_upgrade_cap= + package::test_publish( + object::id_from_address(@0x123456), + test_scenario::ctx(&mut scenario) + ); + + state::init_test_only(ctx(&mut scenario)); + + let pyth_deployer_cap = test_scenario::take_from_address( + &scenario, + @pyth + ); + + state::init_and_share_state( + pyth_deployer_cap, + pyth_upgrade_cap, + stale_price_threshold, + base_update_fee, + data_source::new(governance_emitter_chain_id, external_address::from_bytes(governance_emitter_address)), + data_sources, + ctx(&mut scenario) + ); + + let coins = coin::mint_for_testing(to_mint, ctx(&mut scenario)); + (scenario, coins) + } + + #[test_only] + fun get_mock_price_infos(): vector { + vector[ + price_info::new_price_info( + 1663680747, + 1663074349, + price_feed::new( + price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"), + price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740), + price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740), + ), + ), + price_info::new_price_info( + 1663680747, + 1663074349, + price_feed::new( + price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"), + price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745), + price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745), + ), + ), + price_info::new_price_info( + 1663680747, + 1663074349, + price_feed::new( + price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"), + price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745), + price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745), + ), + ), + price_info::new_price_info( + 1663680747, + 1663074349, + price_feed::new( + price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"), + price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745), + price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745), + ), + ), + ] + } +} \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index bed995e1c0..0cd5254e2d 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -3,12 +3,14 @@ module pyth::state { use sui::object::{Self, UID, ID}; use sui::transfer::{Self}; use sui::tx_context::{Self, TxContext}; + use sui::package::{UpgradeCap}; use pyth::data_source::{Self, DataSource}; use pyth::price_info::{Self}; use pyth::price_identifier::{PriceIdentifier}; friend pyth::pyth; + friend pyth::pyth_tests; friend pyth::governance_action; friend pyth::set_update_fee; friend pyth::set_stale_price_threshold; @@ -28,6 +30,7 @@ module pyth::state { last_executed_governance_sequence: u64, stale_price_threshold: u64, base_update_fee: u64, + upgrade_cap: UpgradeCap } fun init(ctx: &mut TxContext) { @@ -39,9 +42,22 @@ module pyth::state { ); } + #[test_only] + public fun init_test_only(ctx: &mut TxContext) { + init(ctx); + + // This will be created and sent to the transaction sender + // automatically when the contract is published. + transfer::public_transfer( + sui::package::test_publish(object::id_from_address(@pyth), ctx), + tx_context::sender(ctx) + ); + } + // Initialization public(friend) fun init_and_share_state( deployer: DeployerCap, + upgrade_cap: UpgradeCap, stale_price_threshold: u64, base_update_fee: u64, governance_data_source: DataSource, @@ -73,6 +89,7 @@ module pyth::state { transfer::share_object( State { id: uid, + upgrade_cap, governance_data_source, last_executed_governance_sequence: 0, stale_price_threshold, From 4dd4c419252898dae6afaa89f754c4a74e551e83 Mon Sep 17 00:00:00 2001 From: optke3 Date: Sat, 8 Apr 2023 16:03:58 +0000 Subject: [PATCH 13/32] updates --- target_chains/sui/contracts/Move.toml | 2 +- .../sources/governance/governance.move | 10 +- .../sources/governance/set_data_sources.move | 3 +- .../set_governance_data_source.move | 3 +- target_chains/sui/contracts/sources/pyth.move | 106 +++++++++++++++--- 5 files changed, 102 insertions(+), 22 deletions(-) diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index d1fc1dd52c..116588fc4d 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -5,7 +5,7 @@ version = "0.0.1" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" subdir = "crates/sui-framework/packages/sui-framework" -rev = "82c9c80c11488858f1d3930f47ec9f335a566683" +rev = "81dbcf2b6cab07d623a1012bf31daf658963c765" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move index 40eb979b56..d84657a03d 100644 --- a/target_chains/sui/contracts/sources/governance/governance.move +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -1,5 +1,5 @@ module pyth::governance { - use sui::tx_context::{TxContext}; + use sui::clock::{Clock}; use pyth::data_source::{Self}; use pyth::governance_instruction; @@ -24,9 +24,9 @@ module pyth::governance { pyth_state : &mut State, worm_state: &WormState, vaa_bytes: vector, - ctx: &mut TxContext + clock: &Clock ) { - let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, ctx); + let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, clock); let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa)); // Dispatch the instruction to the appropiate handler @@ -53,9 +53,9 @@ module pyth::governance { pyth_state: &mut State, worm_state: &WormState, bytes: vector, - ctx: &mut TxContext + clock: &Clock, ): VAA { - let parsed_vaa = vaa::parse_and_verify(worm_state, bytes, ctx); + let parsed_vaa = vaa::parse_and_verify(worm_state, bytes, clock); // Check that the governance data source is valid assert!( diff --git a/target_chains/sui/contracts/sources/governance/set_data_sources.move b/target_chains/sui/contracts/sources/governance/set_data_sources.move index b1d442592b..f22969bccf 100644 --- a/target_chains/sui/contracts/sources/governance/set_data_sources.move +++ b/target_chains/sui/contracts/sources/governance/set_data_sources.move @@ -3,6 +3,7 @@ module pyth::set_data_sources { use wormhole::cursor; use wormhole::external_address::{Self}; + use wormhole::bytes32::{Self}; use pyth::deserialize; use pyth::data_source::{Self, DataSource}; @@ -28,7 +29,7 @@ module pyth::set_data_sources { let i = 0; while (i < data_sources_count) { let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); - let emitter_address = external_address::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)); + let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32))); vector::push_back(&mut sources, data_source::new((emitter_chain_id as u64), emitter_address)); i = i + 1; diff --git a/target_chains/sui/contracts/sources/governance/set_governance_data_source.move b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move index 77328a8788..4616339b65 100644 --- a/target_chains/sui/contracts/sources/governance/set_governance_data_source.move +++ b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move @@ -5,6 +5,7 @@ module pyth::set_governance_data_source { use wormhole::cursor; use wormhole::external_address::{Self, ExternalAddress}; + use wormhole::bytes32::{Self}; //use wormhole::state::{Self} friend pyth::governance; @@ -24,7 +25,7 @@ module pyth::set_governance_data_source { fun from_byte_vec(bytes: vector): SetGovernanceDataSource { let cursor = cursor::new(bytes); let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); - let emitter_address = external_address::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)); + let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32))); let initial_sequence = deserialize::deserialize_u64(&mut cursor); cursor::destroy_empty(cursor); SetGovernanceDataSource { diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index df94e27cfb..aa8f9967b6 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -13,15 +13,18 @@ module pyth::pyth { use pyth::price_info::{Self, PriceInfo, PriceInfoObject}; use pyth::batch_price_attestation::{Self}; use pyth::price_feed::{Self}; - use pyth::price::{Self}; + use pyth::price::{Self, Price}; + use pyth::price_identifier::{PriceIdentifier}; use wormhole::external_address::{Self}; use wormhole::vaa::{Self}; use wormhole::state::{State as WormState}; + use wormhole::bytes32::{Self}; const E_DATA_SOURCE_EMITTER_ADDRESS_AND_CHAIN_IDS_DIFFERENT_LENGTHS: u64 = 0; const E_INVALID_DATA_SOURCE: u64 = 1; const E_INSUFFICIENT_FEE: u64 = 2; + const E_STALE_PRICE_UPDATE: u64 = 3; /// Call init_and_share_state with deployer cap to initialize /// state and emit event corresponding to Pyth initialization. @@ -43,7 +46,8 @@ module pyth::pyth { update_fee, data_source::new( governance_emitter_chain_id, - external_address::from_bytes(governance_emitter_address)), + external_address::new((bytes32::from_bytes(governance_emitter_address))) + ), parse_data_sources( data_sources_emitter_chain_ids, data_sources_emitter_addresses, @@ -68,7 +72,7 @@ module pyth::pyth { while (i < vector::length(&emitter_chain_ids)) { vector::push_back(&mut sources, data_source::new( *vector::borrow(&emitter_chain_ids, i), - external_address::from_bytes(*vector::borrow(&emitter_addresses, i)) + external_address::new(bytes32::from_bytes(*vector::borrow(&emitter_addresses, i))) )); i = i + 1; @@ -88,7 +92,7 @@ module pyth::pyth { let vaa = vector::pop_back(&mut vaas); // Deserialize the VAA - let vaa = vaa::parse_and_verify(worm_state, vaa, ctx); + let vaa = vaa::parse_and_verify(worm_state, vaa, clock); // Check that the VAA is from a valid data source (emitter) assert!( @@ -147,9 +151,8 @@ module pyth::pyth { vaas: vector>, price_info_objects: &mut vector, fee: Coin, - clock: &Clock, - ctx: &mut TxContext - ) { + clock: &Clock + ){ // Charge the message update fee assert!(get_total_update_fee(pyth_state, &vaas) <= coin::value(&fee), E_INSUFFICIENT_FEE); @@ -163,8 +166,7 @@ module pyth::pyth { pyth_state, vector::pop_back(&mut vaas), price_info_objects, - clock, - ctx + clock ); }; } @@ -177,11 +179,10 @@ module pyth::pyth { pyth_state: &PythState, worm_vaa: vector, price_info_objects: &mut vector, - clock: &Clock, - ctx: &mut TxContext + clock: &Clock ) { // Deserialize the VAA - let vaa = vaa::parse_and_verify(worm_state, worm_vaa, ctx); + let vaa = vaa::parse_and_verify(worm_state, worm_vaa, clock); // Check that the VAA is from a valid data source (emitter) assert!( @@ -254,14 +255,90 @@ module pyth::pyth { update_timestamp > cached_timestamp } + // ----------------------------------------------------------------------------- + // Query the cached prices + // + // It is strongly recommended to update the cached prices using the functions above, + // before using the functions below to query the cached data. + /// Get the number of AptosCoin's required to perform the given price updates. /// + + /// Determine if a price feed for the given price_identifier exists + public fun price_feed_exists(state: &PythState, price_identifier: PriceIdentifier): bool { + state::price_feed_object_exists(state, price_identifier) + } + + /// Get the latest available price cached for the given price identifier, if that price is + /// no older than the stale price threshold. + /// + /// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for + /// how to how this price safely. + /// + /// Important: Pyth uses an on-demand update model, where consumers need to update the + /// cached prices before using them. Please read more about this at https://docs.pyth.network/consume-data/on-demand. + /// get_price() is likely to abort unless you call update_price_feeds() to update the cached price + /// beforehand, as the cached prices may be older than the stale price threshold. + /// + /// The price_info_object is a Sui object with the key ability that uniquely + /// contains a price feed for a given price_identifier. + /// + public fun get_price(state: &PythState, price_info_object: &PriceInfoObject, clock: &Clock): Price { + get_price_no_older_than(price_info_object, clock, state::get_stale_price_threshold_secs(state)) + } + + /// Get the latest available price cached for the given price identifier, if that price is + /// no older than the given age. + public fun get_price_no_older_than(price_info_object: &PriceInfoObject, clock: &Clock, max_age_secs: u64): Price { + let price = get_price_unsafe(price_info_object); + check_price_is_fresh(&price, clock, max_age_secs); + price + } + + /// Get the latest available price cached for the given price identifier. + /// + /// WARNING: the returned price can be from arbitrarily far in the past. + /// This function makes no guarantees that the returned price is recent or + /// useful for any particular application. Users of this function should check + /// the returned timestamp to ensure that the returned price is sufficiently + /// recent for their application. The checked get_price_no_older_than() + /// function should be used in preference to this. + public fun get_price_unsafe(price_info_object: &PriceInfoObject): Price { + // TODO: extract Price from this guy... + let price_info = price_info::get_price_info_from_price_info_object(price_info_object); + price_feed::get_price( + price_info::get_price_feed(&price_info) + ) + } + + fun abs_diff(x: u64, y: u64): u64 { + if (x > y) { + return x - y + } else { + return y - x + } + } + + /// Get the stale price threshold: the amount of time after which a cached price + /// is considered stale and no longer returned by get_price()/get_ema_price(). + public fun get_stale_price_threshold_secs(state: &PythState): u64 { + state::get_stale_price_threshold_secs(state) + } + + fun check_price_is_fresh(price: &Price, clock: &Clock, max_age_secs: u64) { + let age = abs_diff(clock::timestamp_ms(clock)/1000, price::get_timestamp(price)); + assert!(age < max_age_secs, E_STALE_PRICE_UPDATE); + } + /// Please read more information about the update fee here: https://docs.pyth.network/consume-data/on-demand#fees public fun get_total_update_fee(pyth_state: &PythState, update_data: &vector>): u64 { state::get_base_update_fee(pyth_state) * vector::length(update_data) } } + + + // TODO - pyth tests // https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/aptos/contracts/sources/pyth.move#L384 @@ -283,6 +360,7 @@ module pyth::pyth_tests{ use wormhole::setup::{Self as wormhole_setup, DeployerCap}; use wormhole::external_address::{Self}; + use wormhole::bytes32::{Self}; #[test_only] /// Init Wormhole core bridge state. @@ -332,7 +410,7 @@ module pyth::pyth_tests{ let guardian_set_seconds_to_live = 5678; let message_fee = 350; - wormhole_setup::init_and_share_state( + wormhole_setup::complete( deployer_cap, upgrade_cap, governance_chain, @@ -365,7 +443,7 @@ module pyth::pyth_tests{ pyth_upgrade_cap, stale_price_threshold, base_update_fee, - data_source::new(governance_emitter_chain_id, external_address::from_bytes(governance_emitter_address)), + data_source::new(governance_emitter_chain_id, external_address::new(bytes32::from_bytes(governance_emitter_address))), data_sources, ctx(&mut scenario) ); From b8877f64e93bfdc3c8c532dd686a4e5a616334b8 Mon Sep 17 00:00:00 2001 From: optke3 Date: Sun, 9 Apr 2023 18:35:32 +0000 Subject: [PATCH 14/32] test_get_update_fee test passes --- target_chains/sui/contracts/sources/pyth.move | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index aa8f9967b6..a0b92ff396 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -261,9 +261,6 @@ module pyth::pyth { // It is strongly recommended to update the cached prices using the functions above, // before using the functions below to query the cached data. - /// Get the number of AptosCoin's required to perform the given price updates. - /// - /// Determine if a price feed for the given price_identifier exists public fun price_feed_exists(state: &PythState, price_identifier: PriceIdentifier): bool { state::price_feed_object_exists(state, price_identifier) @@ -336,27 +333,22 @@ module pyth::pyth { } } - - - -// TODO - pyth tests -// https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/aptos/contracts/sources/pyth.move#L384 - module pyth::pyth_tests{ use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::clock::{Self}; - use sui::test_scenario::{Self, Scenario, ctx}; + use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared}; use sui::package::Self; use sui::object::Self; - use pyth::state::{Self}; + use pyth::state::{Self, State as PythState}; use pyth::price_identifier::{Self}; use pyth::price_info::{Self, PriceInfo}; use pyth::price_feed::{Self}; use pyth::data_source::{Self, DataSource}; use pyth::i64::{Self}; use pyth::price::{Self}; + use pyth::pyth::{Self}; use wormhole::setup::{Self as wormhole_setup, DeployerCap}; use wormhole::external_address::{Self}; @@ -493,4 +485,29 @@ module pyth::pyth_tests{ ), ] } + + #[test_only] + fun test_get_update_fee() { + let single_update_fee = 50; + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 100000, 0); + + let pyth_state = take_shared(&scenario); + // Pass in a single VAA + assert!(pyth::get_total_update_fee(&pyth_state, &vector[ + x"fb1543888001083cf2e6ef3afdcf827e89b11efd87c563638df6e1995ada9f93", + ]) == single_update_fee, 1); + + // Pass in multiple VAAs + assert!(pyth::get_total_update_fee(&pyth_state, &vector[ + x"4ee17a1a4524118de513fddcf82b77454e51be5d6fc9e29fc72dd6c204c0e4fa", + x"c72fdf81cfc939d4286c93fbaaae2eec7bae28a5926fa68646b43a279846ccc1", + x"d9a8123a793529c31200339820a3210059ecace6c044f81ecad62936e47ca049", + x"84e4f21b3e65cef47fda25d15b4eddda1edf720a1d062ccbf441d6396465fbe6", + x"9e73f9041476a93701a0b9c7501422cc2aa55d16100bec628cf53e0281b6f72f" + ]) == 250, 1); + + return_shared(pyth_state); + coin::burn_for_testing(test_coins); + test_scenario::end(scenario); + } } \ No newline at end of file From 5dcb6137d796274b416ace09efd92b60a44f6a14 Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 16:39:17 +0000 Subject: [PATCH 15/32] fix test_get_update_fee and test_update_price_feeds_corrupt_vaa --- target_chains/sui/contracts/Move.lock | 29 +++++++++ target_chains/sui/contracts/sources/pyth.move | 59 ++++++++++++++----- .../sui/contracts/sources/state.move | 14 ++++- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 target_chains/sui/contracts/Move.lock diff --git a/target_chains/sui/contracts/Move.lock b/target_chains/sui/contracts/Move.lock new file mode 100644 index 0000000000..b892597451 --- /dev/null +++ b/target_chains/sui/contracts/Move.lock @@ -0,0 +1,29 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 + +dependencies = [ + { name = "Sui" }, + { name = "Wormhole" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "81dbcf2b6cab07d623a1012bf31daf658963c765", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "81dbcf2b6cab07d623a1012bf31daf658963c765", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[[move.package]] +name = "Wormhole" +source = { git = "https://github.com/wormhole-foundation/wormhole.git", rev = "sui/integration_v2", subdir = "sui/wormhole" } + +dependencies = [ + { name = "Sui" }, +] diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index a0b92ff396..27dfe73f97 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -336,7 +336,7 @@ module pyth::pyth { module pyth::pyth_tests{ use sui::sui::SUI; use sui::coin::{Self, Coin}; - use sui::clock::{Self}; + use sui::clock::{Self, Clock}; use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared}; use sui::package::Self; use sui::object::Self; @@ -353,11 +353,14 @@ module pyth::pyth_tests{ use wormhole::setup::{Self as wormhole_setup, DeployerCap}; use wormhole::external_address::{Self}; use wormhole::bytes32::{Self}; + use wormhole::state::{State as WormState}; + + const DEPLOYER: address = @0x1234; #[test_only] /// Init Wormhole core bridge state. /// Init Pyth state. - /// Set initial Sui clock time, + /// Set initial Sui clock time. /// Mint some SUI fee coins. fun setup_test( stale_price_threshold: u64, @@ -368,17 +371,17 @@ module pyth::pyth_tests{ to_mint: u64 ): (Scenario, Coin) { - let deployer = @0x1234; - let scenario = test_scenario::begin(deployer); + let scenario = test_scenario::begin(DEPLOYER); // Initialize Wormhole core bridge. wormhole_setup::init_test_only(ctx(&mut scenario)); - + //debug::print(&0x1111); + test_scenario::next_tx(&mut scenario, DEPLOYER); // Take the `DeployerCap` from the sender of the transaction. let deployer_cap = test_scenario::take_from_address( &scenario, - deployer + DEPLOYER ); // This will be created and sent to the transaction sender automatically @@ -386,7 +389,7 @@ module pyth::pyth_tests{ // it from the sender. let upgrade_cap = package::test_publish( - object::id_from_address(@0x0), + object::id_from_address(@wormhole), test_scenario::ctx(&mut scenario) ); @@ -419,15 +422,15 @@ module pyth::pyth_tests{ // Initialize Pyth state. let pyth_upgrade_cap= package::test_publish( - object::id_from_address(@0x123456), + object::id_from_address(@pyth), test_scenario::ctx(&mut scenario) ); state::init_test_only(ctx(&mut scenario)); - + test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_deployer_cap = test_scenario::take_from_address( &scenario, - @pyth + DEPLOYER ); state::init_and_share_state( @@ -486,11 +489,11 @@ module pyth::pyth_tests{ ] } - #[test_only] + #[test] fun test_get_update_fee() { let single_update_fee = 50; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 100000, 0); - + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], single_update_fee, 0); + test_scenario::next_tx(&mut scenario, DEPLOYER, ); let pyth_state = take_shared(&scenario); // Pass in a single VAA assert!(pyth::get_total_update_fee(&pyth_state, &vector[ @@ -510,4 +513,32 @@ module pyth::pyth_tests{ coin::burn_for_testing(test_coins); test_scenario::end(scenario); } -} \ No newline at end of file + + #[test] + #[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)] + fun test_update_price_feeds_corrupt_vaa() { + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 100000, 0); + test_scenario::next_tx(&mut scenario, DEPLOYER); + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + // Pass in a corrupt VAA, which should fail deseriaizing + let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; + + // Create Pyth price feed + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + vector[corrupt_vaa], + &clock, + ctx(&mut scenario) + ); + + return_shared(pyth_state); + return_shared(worm_state); + return_shared(clock); + coin::burn_for_testing(test_coins); + test_scenario::end(scenario); + } +} diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index 0cd5254e2d..ff9a7c0bfb 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -3,12 +3,14 @@ module pyth::state { use sui::object::{Self, UID, ID}; use sui::transfer::{Self}; use sui::tx_context::{Self, TxContext}; - use sui::package::{UpgradeCap}; + use sui::package::{Self, UpgradeCap}; use pyth::data_source::{Self, DataSource}; use pyth::price_info::{Self}; use pyth::price_identifier::{PriceIdentifier}; + use wormhole::setup::{assert_package_upgrade_cap}; + friend pyth::pyth; friend pyth::pyth_tests; friend pyth::governance_action; @@ -64,9 +66,19 @@ module pyth::state { sources: vector, ctx: &mut TxContext ) { + // TODO - version control + // let version = wormhole::version_control::version(); + //assert!(version == 1, E_INVALID_BUILD_VERSION); + let DeployerCap { id } = deployer; object::delete(id); + assert_package_upgrade_cap( + &upgrade_cap, + package::compatible_policy(), + 1 // version + ); + let uid = object::new(ctx); // Create a set that contains all registered data sources and From edf67353dfb96ccae0479603ba399647b99ee8fc Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 17:28:28 +0000 Subject: [PATCH 16/32] test_update_price_feeds_invalid_data_source --- target_chains/sui/contracts/sources/pyth.move | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 27dfe73f97..adcf9e4a52 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -357,6 +357,14 @@ module pyth::pyth_tests{ const DEPLOYER: address = @0x1234; + #[test_only] + /// A vector containing a single VAA with: + /// - emitter chain ID 17 + /// - emitter address 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b + /// - payload corresponding to the batch price attestation of the prices returned by get_mock_price_infos() + const TEST_VAAS: vector> = vector[x"0100000000010036eb563b80a24f4253bee6150eb8924e4bdf6e4fa1dfc759a6664d2e865b4b134651a7b021b7f1ce3bd078070b688b6f2e37ce2de0d9b48e6a78684561e49d5201527e4f9b00000001001171f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b0000000000000001005032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"]; + + #[test_only] /// Init Wormhole core bridge state. /// Init Pyth state. @@ -398,9 +406,10 @@ module pyth::pyth_tests{ x"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; let initial_guardians = vector[ - x"1337133713371337133713371337133713371337", - x"c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de", - x"ba5edba5edba5edba5edba5edba5edba5edba5ed" + x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe" + //x"1337133713371337133713371337133713371337", + //x"c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de", + //x"ba5edba5edba5edba5edba5edba5edba5edba5ed" ]; let guardian_set_seconds_to_live = 5678; let message_fee = 350; @@ -517,7 +526,7 @@ module pyth::pyth_tests{ #[test] #[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)] fun test_update_price_feeds_corrupt_vaa() { - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 100000, 0); + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); @@ -541,4 +550,39 @@ module pyth::pyth_tests{ coin::burn_for_testing(test_coins); test_scenario::end(scenario); } + + #[test] + #[expected_failure(abort_code = pyth::pyth::E_INVALID_DATA_SOURCE)] + fun test_update_price_feeds_invalid_data_source() { + // Initialize the contract with some valid data sources, excluding our test VAA's source + let data_sources = vector[ + data_source::new( + 4, external_address::new(bytes32::new(x"0000000000000000000000000000000000000000000000000000000000007742")) + ), + data_source::new( + 5, external_address::new(bytes32::new(x"0000000000000000000000000000000000000000000000000000000000007637")) + ) + ]; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + + return_shared(pyth_state); + return_shared(worm_state); + return_shared(clock); + coin::burn_for_testing(test_coins); + test_scenario::end(scenario); + } } From 48ccac678ce70e4bfbf2eb8e8ebdca768d2f79ea Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 18:34:55 +0000 Subject: [PATCH 17/32] test_create_and_update_price_feeds --- target_chains/sui/contracts/Move.toml | 2 +- target_chains/sui/contracts/sources/pyth.move | 71 +++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index 116588fc4d..82e23e229d 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -5,7 +5,7 @@ version = "0.0.1" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" subdir = "crates/sui-framework/packages/sui-framework" -rev = "81dbcf2b6cab07d623a1012bf31daf658963c765" +rev = "ddfc3fa0768a38286787319603a5458a9ff91cc1" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index adcf9e4a52..8e8cbba71a 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -230,7 +230,8 @@ module pyth::pyth { update ); } - } + }; + i = i + 1; }; if (!found){ // TODO - throw error, since the price_feeds in price_info_objects do @@ -334,6 +335,8 @@ module pyth::pyth { } module pyth::pyth_tests{ + use std::vector::{Self}; + use sui::sui::SUI; use sui::coin::{Self, Coin}; use sui::clock::{Self, Clock}; @@ -343,7 +346,7 @@ module pyth::pyth_tests{ use pyth::state::{Self, State as PythState}; use pyth::price_identifier::{Self}; - use pyth::price_info::{Self, PriceInfo}; + use pyth::price_info::{Self, PriceInfo, PriceInfoObject}; use pyth::price_feed::{Self}; use pyth::data_source::{Self, DataSource}; use pyth::i64::{Self}; @@ -364,7 +367,6 @@ module pyth::pyth_tests{ /// - payload corresponding to the batch price attestation of the prices returned by get_mock_price_infos() const TEST_VAAS: vector> = vector[x"0100000000010036eb563b80a24f4253bee6150eb8924e4bdf6e4fa1dfc759a6664d2e865b4b134651a7b021b7f1ce3bd078070b688b6f2e37ce2de0d9b48e6a78684561e49d5201527e4f9b00000001001171f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b0000000000000001005032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"]; - #[test_only] /// Init Wormhole core bridge state. /// Init Pyth state. @@ -383,7 +385,6 @@ module pyth::pyth_tests{ // Initialize Wormhole core bridge. wormhole_setup::init_test_only(ctx(&mut scenario)); - //debug::print(&0x1111); test_scenario::next_tx(&mut scenario, DEPLOYER); // Take the `DeployerCap` from the sender of the transaction. let deployer_cap = @@ -525,7 +526,7 @@ module pyth::pyth_tests{ #[test] #[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)] - fun test_update_price_feeds_corrupt_vaa() { + fun test_create_price_feeds_corrupt_vaa() { let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); @@ -553,7 +554,7 @@ module pyth::pyth_tests{ #[test] #[expected_failure(abort_code = pyth::pyth::E_INVALID_DATA_SOURCE)] - fun test_update_price_feeds_invalid_data_source() { + fun test_create_price_feeds_invalid_data_source() { // Initialize the contract with some valid data sources, excluding our test VAA's source let data_sources = vector[ data_source::new( @@ -585,4 +586,62 @@ module pyth::pyth_tests{ coin::burn_for_testing(test_coins); test_scenario::end(scenario); } + + #[test_only] + fun data_sources_for_test_vaa(): vector { + // Set some valid data sources, including our test VAA's source + vector[ + data_source::new( + 1, external_address::new(bytes32::from_bytes(x"0000000000000000000000000000000000000000000000000000000000000004"))), + data_source::new( + 5, external_address::new(bytes32::new(x"0000000000000000000000000000000000000000000000000000000000007637"))), + data_source::new( + 17, external_address::new(bytes32::new(x"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"))) + ] + } + + #[test] + fun test_create_and_update_price_feeds() { + let data_sources = data_sources_for_test_vaa(); + let base_update_fee = 50; + let coins_to_mint = 5000; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let price_info_object = take_shared(&scenario); + let w = vector[price_info_object]; + + pyth::update_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &mut w, + test_coins, + &clock + ); + + price_info_object = vector::pop_back(&mut w); + vector::destroy_empty(w); + return_shared(pyth_state); + return_shared(worm_state); + return_shared(price_info_object); + return_shared(clock); + test_scenario::end(scenario); + } + } From 1c2dd4a2be014e92ff524e7f5e36ae4d0713555b Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 19:31:14 +0000 Subject: [PATCH 18/32] test_create_and_update_price_feeds_success and test_create_and_update_price_feeds_price_info_object_not_found_failure --- target_chains/sui/contracts/sources/pyth.move | 172 +++++++++++++++++- 1 file changed, 163 insertions(+), 9 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 8e8cbba71a..f8e246e3a6 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -25,6 +25,7 @@ module pyth::pyth { const E_INVALID_DATA_SOURCE: u64 = 1; const E_INSUFFICIENT_FEE: u64 = 2; const E_STALE_PRICE_UPDATE: u64 = 3; + const E_PRICE_INFO_OBJECT_NOT_FOUND: u64 = 4; /// Call init_and_share_state with deployer cap to initialize /// state and emit event corresponding to Pyth initialization. @@ -234,8 +235,7 @@ module pyth::pyth { i = i + 1; }; if (!found){ - // TODO - throw error, since the price_feeds in price_info_objects do - // not constitute a superset of the price_feeds to be updated + abort(E_PRICE_INFO_OBJECT_NOT_FOUND) } }; vector::destroy_empty(updates); @@ -342,7 +342,7 @@ module pyth::pyth_tests{ use sui::clock::{Self, Clock}; use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared}; use sui::package::Self; - use sui::object::Self; + use sui::object::{Self, ID}; use pyth::state::{Self, State as PythState}; use pyth::price_identifier::{Self}; @@ -499,6 +499,30 @@ module pyth::pyth_tests{ ] } + #[test_only] + fun check_price_feeds_cached(expected: &vector, actual: &vector) { + // Check that we can retrieve the correct current price and ema price for each price feed + let i = 0; + while (i < vector::length(expected)) { + let price_feed = price_info::get_price_feed(vector::borrow(expected, i)); + let price = price_feed::get_price(price_feed); + let ema_price = price_feed::get_ema_price(price_feed); + let price_identifier = price_info::get_price_identifier(vector::borrow(expected, i)); + + let actual_price_info = price_info::get_price_info_from_price_info_object(vector::borrow(actual, i)); + let actual_price_feed = price_info::get_price_feed(&actual_price_info); + let actual_price = price_feed::get_price(actual_price_feed); + let actual_ema_price = price_feed::get_ema_price(actual_price_feed); + let actual_price_identifier = price_info::get_price_identifier(&actual_price_info); + + assert!(price == actual_price, 0); + assert!(ema_price == actual_ema_price, 0); + assert!(price_identifier::get_bytes(&price_identifier) == price_identifier::get_bytes(&actual_price_identifier), 0); + + i = i + 1; + }; + } + #[test] fun test_get_update_fee() { let single_update_fee = 50; @@ -565,7 +589,7 @@ module pyth::pyth_tests{ ) ]; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); @@ -601,7 +625,7 @@ module pyth::pyth_tests{ } #[test] - fun test_create_and_update_price_feeds() { + fun test_create_and_update_price_feeds_success() { let data_sources = data_sources_for_test_vaa(); let base_update_fee = 50; let coins_to_mint = 5000; @@ -621,22 +645,152 @@ module pyth::pyth_tests{ ctx(&mut scenario) ); + // Affirm that 4 objects, which correspond to the 4 new price info objects + // containing the price feeds were created and shared. + let effects = test_scenario::next_tx(&mut scenario, DEPLOYER); + let shared_ids = test_scenario::shared(&effects); + let created_ids = test_scenario::created(&effects); + assert!(vector::length(&shared_ids)==4, 0); + assert!(vector::length(&created_ids)==4, 0); + + let price_info_object_1 = take_shared(&scenario); + let price_info_object_2 = take_shared(&scenario); + let price_info_object_3 = take_shared(&scenario); + let price_info_object_4 = take_shared(&scenario); + + // Create vector of price info objects (Sui objects with key ability and living in global store), + // which contain the price feeds we want to update. Note that these can be passed into + // update_price_feeds in any order! + let price_info_object_vec = vector[price_info_object_1, price_info_object_2, price_info_object_3, price_info_object_4]; + + pyth::update_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &mut price_info_object_vec, + test_coins, + &clock + ); + + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_4 = vector::pop_back(&mut price_info_object_vec); + + vector::destroy_empty(price_info_object_vec); + return_shared(pyth_state); + return_shared(worm_state); + return_shared(price_info_object_1); + return_shared(price_info_object_2); + return_shared(price_info_object_3); + return_shared(price_info_object_4); + + return_shared(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = pyth::pyth::E_PRICE_INFO_OBJECT_NOT_FOUND)] + fun test_create_and_update_price_feeds_price_info_object_not_found_failure() { + let data_sources = data_sources_for_test_vaa(); + let base_update_fee = 50; + let coins_to_mint = 5000; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + + // Affirm that 4 objects, which correspond to the 4 new price info objects + // containing the price feeds were created and shared. + let effects = test_scenario::next_tx(&mut scenario, DEPLOYER); + let shared_ids = test_scenario::shared(&effects); + let created_ids = test_scenario::created(&effects); + assert!(vector::length(&shared_ids)==4, 0); + assert!(vector::length(&created_ids)==4, 0); + + let price_info_object_1 = take_shared(&scenario); + let price_info_object_2 = take_shared(&scenario); + let price_info_object_3 = take_shared(&scenario); + let price_info_object_4 = take_shared(&scenario); + + // Note that here we only pass in 3 price info objects corresponding to 3 out + // of the 4 price feeds. + let price_info_object_vec = vector[price_info_object_1, price_info_object_2, price_info_object_3]; + + pyth::update_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &mut price_info_object_vec, + test_coins, + &clock + ); + + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + + vector::destroy_empty(price_info_object_vec); + return_shared(pyth_state); + return_shared(worm_state); + return_shared(price_info_object_1); + return_shared(price_info_object_2); + return_shared(price_info_object_3); + return_shared(price_info_object_4); + + return_shared(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = pyth::pyth::E_INSUFFICIENT_FEE)] + fun test_create_and_update_price_feeds_insufficient_fee() { + let data_sources = data_sources_for_test_vaa(); + let base_update_fee = 50; + let coins_to_mint = 5; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + test_scenario::next_tx(&mut scenario, DEPLOYER); let price_info_object = take_shared(&scenario); - let w = vector[price_info_object]; + let price_info_object_vec = vector[price_info_object]; pyth::update_price_feeds( &mut worm_state, &mut pyth_state, TEST_VAAS, - &mut w, + &mut price_info_object_vec, test_coins, &clock ); - price_info_object = vector::pop_back(&mut w); - vector::destroy_empty(w); + price_info_object = vector::pop_back(&mut price_info_object_vec); + vector::destroy_empty(price_info_object_vec); return_shared(pyth_state); return_shared(worm_state); return_shared(price_info_object); From b1b66241cabd8158ae3e53bcf3563676fc348772 Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 19:41:09 +0000 Subject: [PATCH 19/32] test_update_cache --- target_chains/sui/contracts/sources/pyth.move | 71 ++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index f8e246e3a6..a5b10fee53 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -27,6 +27,8 @@ module pyth::pyth { const E_STALE_PRICE_UPDATE: u64 = 3; const E_PRICE_INFO_OBJECT_NOT_FOUND: u64 = 4; + friend pyth::pyth_tests; + /// Call init_and_share_state with deployer cap to initialize /// state and emit event corresponding to Pyth initialization. public entry fun init_pyth( @@ -213,8 +215,9 @@ module pyth::pyth { let i = 0; let found = false; // Find PriceInfoObjects corresponding to the current update (PriceInfo). - // TODO - This for loop might be expensive if there are a large - // number of updates and/or price_info_objects we are updating. + // TODO - Construct an in-memory table to make look-ups faster? + // This loop might be expensive if there are a large number + // of updates and/or price_info_objects we are updating. while (i < vector::length(price_info_objects)){ // Check if the current price info object corresponds to the price feed that // the update is meant for. @@ -676,8 +679,8 @@ module pyth::pyth_tests{ price_info_object_2 = vector::pop_back(&mut price_info_object_vec); price_info_object_3 = vector::pop_back(&mut price_info_object_vec); price_info_object_4 = vector::pop_back(&mut price_info_object_vec); - vector::destroy_empty(price_info_object_vec); + return_shared(pyth_state); return_shared(worm_state); return_shared(price_info_object_1); @@ -798,4 +801,66 @@ module pyth::pyth_tests{ test_scenario::end(scenario); } + #[test] + fun test_update_cache(){ + let data_sources = data_sources_for_test_vaa(); + let base_update_fee = 50; + let coins_to_mint = 5000; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + + // Affirm that 4 objects, which correspond to the 4 new price info objects + // containing the price feeds were created and shared. + let effects = test_scenario::next_tx(&mut scenario, DEPLOYER); + let shared_ids = test_scenario::shared(&effects); + let created_ids = test_scenario::created(&effects); + assert!(vector::length(&shared_ids)==4, 0); + assert!(vector::length(&created_ids)==4, 0); + + let price_info_object_1 = take_shared(&scenario); + let price_info_object_2 = take_shared(&scenario); + let price_info_object_3 = take_shared(&scenario); + let price_info_object_4 = take_shared(&scenario); + + let updates = get_mock_price_infos(); + let price_info_object_vec = vector[ + price_info_object_1, + price_info_object_2, + price_info_object_3, + price_info_object_4 + ]; + + check_price_feeds_cached(&updates, &price_info_object_vec); + + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_4 = vector::pop_back(&mut price_info_object_vec); + vector::destroy_empty(price_info_object_vec); + + return_shared(pyth_state); + return_shared(worm_state); + return_shared(price_info_object_1); + return_shared(price_info_object_2); + return_shared(price_info_object_3); + return_shared(price_info_object_4); + coin::burn_for_testing(test_coins); + + return_shared(clock); + test_scenario::end(scenario); + } + } From a9e37b49c4f0e1e6bcf45d1aab761fd057f8989d Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 19:41:56 +0000 Subject: [PATCH 20/32] update --- target_chains/sui/contracts/sources/pyth.move | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index a5b10fee53..9a1c701095 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -822,13 +822,7 @@ module pyth::pyth_tests{ ctx(&mut scenario) ); - // Affirm that 4 objects, which correspond to the 4 new price info objects - // containing the price feeds were created and shared. - let effects = test_scenario::next_tx(&mut scenario, DEPLOYER); - let shared_ids = test_scenario::shared(&effects); - let created_ids = test_scenario::created(&effects); - assert!(vector::length(&shared_ids)==4, 0); - assert!(vector::length(&created_ids)==4, 0); + test_scenario::next_tx(&mut scenario, DEPLOYER); let price_info_object_1 = take_shared(&scenario); let price_info_object_2 = take_shared(&scenario); From 2965e29dffa13f7178feedee23f348ce0fb92613 Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 21:35:32 +0000 Subject: [PATCH 21/32] test_update_cache_old_update --- target_chains/sui/contracts/sources/pyth.move | 147 ++++++++++++++++-- 1 file changed, 138 insertions(+), 9 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 9a1c701095..6a6d539c73 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -205,7 +205,7 @@ module pyth::pyth { } /// Update PriceInfoObjects using up-to-date PriceInfos. - fun update_cache( + public(friend) fun update_cache( updates: vector, price_info_objects: &mut vector, clock: &Clock, @@ -218,7 +218,7 @@ module pyth::pyth { // TODO - Construct an in-memory table to make look-ups faster? // This loop might be expensive if there are a large number // of updates and/or price_info_objects we are updating. - while (i < vector::length(price_info_objects)){ + while (i < vector::length(price_info_objects) && found == false){ // Check if the current price info object corresponds to the price feed that // the update is meant for. let price_info = price_info::get_price_info_from_price_info_object(vector::borrow(price_info_objects, i)); @@ -503,6 +503,7 @@ module pyth::pyth_tests{ } #[test_only] + /// Compare the expected price feed with the actual Pyth price feeds. fun check_price_feeds_cached(expected: &vector, actual: &vector) { // Check that we can retrieve the correct current price and ema price for each price feed let i = 0; @@ -675,10 +676,10 @@ module pyth::pyth_tests{ &clock ); - price_info_object_1 = vector::pop_back(&mut price_info_object_vec); - price_info_object_2 = vector::pop_back(&mut price_info_object_vec); - price_info_object_3 = vector::pop_back(&mut price_info_object_vec); price_info_object_4 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); vector::destroy_empty(price_info_object_vec); return_shared(pyth_state); @@ -740,9 +741,9 @@ module pyth::pyth_tests{ &clock ); - price_info_object_1 = vector::pop_back(&mut price_info_object_vec); - price_info_object_2 = vector::pop_back(&mut price_info_object_vec); price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); vector::destroy_empty(price_info_object_vec); return_shared(pyth_state); @@ -829,6 +830,7 @@ module pyth::pyth_tests{ let price_info_object_3 = take_shared(&scenario); let price_info_object_4 = take_shared(&scenario); + // These updates are price infos that correspond to the ones in TEST_VAAS. let updates = get_mock_price_infos(); let price_info_object_vec = vector[ price_info_object_1, @@ -837,14 +839,142 @@ module pyth::pyth_tests{ price_info_object_4 ]; + // Check that TEST_VAAS was indeed used to instantiate the price feeds correctly, + // by confirming that the info in updates is contained in price_info_object_vec. check_price_feeds_cached(&updates, &price_info_object_vec); - price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + price_info_object_4 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + vector::destroy_empty(price_info_object_vec); + + return_shared(pyth_state); + return_shared(worm_state); + return_shared(price_info_object_1); + return_shared(price_info_object_2); + return_shared(price_info_object_3); + return_shared(price_info_object_4); + coin::burn_for_testing(test_coins); + + return_shared(clock); + test_scenario::end(scenario); + } + + #[test] + fun test_update_cache_old_update() { + let data_sources = data_sources_for_test_vaa(); + let base_update_fee = 50; + let coins_to_mint = 5000; + + let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let pyth_state = take_shared(&scenario); + let worm_state = take_shared(&scenario); + let clock = take_shared(&scenario); + + pyth::create_price_feeds( + &mut worm_state, + &mut pyth_state, + TEST_VAAS, + &clock, + ctx(&mut scenario) + ); + + test_scenario::next_tx(&mut scenario, DEPLOYER); + + let price_info_object_1 = take_shared(&scenario); + let price_info_object_2 = take_shared(&scenario); + let price_info_object_3 = take_shared(&scenario); + let price_info_object_4 = take_shared(&scenario); + + let price_info_object_vec = vector[ + price_info_object_1, + price_info_object_2, + price_info_object_3, + price_info_object_4 + ]; + + // Hardcode the price identifier, price, and ema_price for price_info_object_1, because + // it's easier than unwrapping price_info_object_1 and getting the quantities via getters. + let timestamp = 1663680740; + let price_identifier = price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"); + let price = price::new(i64::new(1557, false), 7, i64::new(5, true), timestamp); + let ema_price = price::new(i64::new(1500, false), 3, i64::new(5, true), timestamp); + + // Attempt to update the price with an update older than the current cached one. + let old_price = price::new(i64::new(1243, true), 9802, i64::new(6, false), timestamp - 200); + let old_ema_price = price::new(i64::new(8976, true), 234, i64::new(897, false), timestamp - 200); + let old_update = price_info::new_price_info( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + old_price, + old_ema_price, + ) + ); + pyth::update_cache(vector[old_update], &mut price_info_object_vec, &clock); + + price_info_object_4 = vector::pop_back(&mut price_info_object_vec); price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + + vector::destroy_empty(price_info_object_vec); + + // Confirm that the current price and ema price didn't change + let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); + let current_price_feed = price_info::get_price_feed(¤t_price_info); + let current_price = price_feed::get_price(current_price_feed); + let current_ema_price = price_feed::get_ema_price(current_price_feed); + + // Confirm that no price update occurred when we tried to update cache with an + // outdated update: old_update. + assert!(current_price == price, 1); + assert!(current_ema_price == ema_price, 1); + + test_scenario::next_tx(&mut scenario, DEPLOYER); + + // Update the cache with a fresh update. + let fresh_price = price::new(i64::new(5243, true), 2, i64::new(3, false), timestamp + 200); + let fresh_ema_price = price::new(i64::new(8976, true), 21, i64::new(32, false), timestamp + 200); + let fresh_update = price_info::new_price_info( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + fresh_price, + fresh_ema_price, + ) + ); + + let price_info_object_vec = vector[ + price_info_object_1, + price_info_object_2, + price_info_object_3, + price_info_object_4 + ]; + + pyth::update_cache(vector[fresh_update], &mut price_info_object_vec, &clock); + price_info_object_4 = vector::pop_back(&mut price_info_object_vec); + price_info_object_3 = vector::pop_back(&mut price_info_object_vec); + price_info_object_2 = vector::pop_back(&mut price_info_object_vec); + price_info_object_1 = vector::pop_back(&mut price_info_object_vec); + vector::destroy_empty(price_info_object_vec); + // Confirm that the Pyth cached price got updated to fresh_price + let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); + let current_price_feed = price_info::get_price_feed(¤t_price_info); + let current_price = price_feed::get_price(current_price_feed); + let current_ema_price = price_feed::get_ema_price(current_price_feed); + + assert!(current_price==fresh_price, 0); + assert!(current_ema_price==fresh_ema_price, 0); + return_shared(pyth_state); return_shared(worm_state); return_shared(price_info_object_1); @@ -856,5 +986,4 @@ module pyth::pyth_tests{ return_shared(clock); test_scenario::end(scenario); } - } From af364b8978f7ec66b59e471685bbeb74a11a5f4e Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 22:02:51 +0000 Subject: [PATCH 22/32] update_price_feeds_if_fresh --- target_chains/sui/contracts/sources/pyth.move | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 6a6d539c73..898c32739f 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -26,6 +26,8 @@ module pyth::pyth { const E_INSUFFICIENT_FEE: u64 = 2; const E_STALE_PRICE_UPDATE: u64 = 3; const E_PRICE_INFO_OBJECT_NOT_FOUND: u64 = 4; + const E_INVALID_PUBLISH_TIMES_LENGTH: u64 = 5; + const E_NO_FRESH_DATA: u64 = 6; friend pyth::pyth_tests; @@ -140,7 +142,7 @@ module pyth::pyth { /// /// The vaas argument is a vector of VAAs encoded as bytes. /// - /// The javascript https://github.com/pyth-network/pyth-js/tree/main/pyth-aptos-js package + /// The javascript https://github.com/pyth-network/pyth-js/tree/main/pyth-sui-js package /// should be used to fetch these VAAs from the Price Service. More information about this /// process can be found at https://docs.pyth.network/consume-data. /// @@ -244,6 +246,48 @@ module pyth::pyth { vector::destroy_empty(updates); } + /// Update the cached price feeds with the data in the given VAAs, using + /// update_price_feeds(). However, this function will only have an effect if any of the + /// prices in the update are fresh. The price_identifiers and publish_times parameters + /// are used to determine if the update is fresh without doing any serialisation or verification + /// of the VAAs, potentially saving time and gas. If the update contains no fresh data, this function + /// will revert with error::no_fresh_data(). + /// + /// For a given price update i in the batch, that price is considered fresh if the current cached + /// price for price_identifiers[i] is older than publish_times[i]. + public fun update_price_feeds_if_fresh( + vaas: vector>, + worm_state: &WormState, + pyth_state: &PythState, + price_info_objects: &mut vector, + publish_times: vector, + fee: Coin, + clock: &Clock + ) { + assert!(vector::length(price_info_objects) == vector::length(&publish_times), + E_INVALID_PUBLISH_TIMES_LENGTH + ); + + let fresh_data = false; + let i = 0; + while (i < vector::length(&publish_times)) { + let cur_price_info = price_info::get_price_info_from_price_info_object(vector::borrow(price_info_objects, i)); + let cur_price_feed = price_info::get_price_feed(&cur_price_info); + let cur_price = price_feed::get_price(cur_price_feed); + + let cached_timestamp = price::get_timestamp(&cur_price); + if (cached_timestamp < *vector::borrow(&publish_times, i)) { + fresh_data = true; + break + }; + + i = i + 1; + }; + + assert!(fresh_data, E_NO_FRESH_DATA); + update_price_feeds(worm_state, pyth_state, vaas, price_info_objects, fee, clock); + } + /// Determine if the given price update is "fresh": we have nothing newer already cached for that /// price feed within a PriceInfoObject. fun is_fresh_update(update: &PriceInfo, price_info_object: &PriceInfoObject): bool { @@ -411,9 +455,6 @@ module pyth::pyth_tests{ let initial_guardians = vector[ x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe" - //x"1337133713371337133713371337133713371337", - //x"c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de", - //x"ba5edba5edba5edba5edba5edba5edba5edba5ed" ]; let guardian_set_seconds_to_live = 5678; let message_fee = 350; @@ -502,8 +543,8 @@ module pyth::pyth_tests{ ] } - #[test_only] - /// Compare the expected price feed with the actual Pyth price feeds. + #[test_only] + /// Compare the expected price feed with the actual Pyth price feeds. fun check_price_feeds_cached(expected: &vector, actual: &vector) { // Check that we can retrieve the correct current price and ema price for each price feed let i = 0; From c3b8858f8cf42f3ad6e17704538de15e2bdbd57c Mon Sep 17 00:00:00 2001 From: optke3 Date: Mon, 10 Apr 2023 22:07:41 +0000 Subject: [PATCH 23/32] comment --- target_chains/sui/contracts/sources/price_info.move | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/target_chains/sui/contracts/sources/price_info.move b/target_chains/sui/contracts/sources/price_info.move index cd98aaefc3..3c258fcba0 100644 --- a/target_chains/sui/contracts/sources/price_info.move +++ b/target_chains/sui/contracts/sources/price_info.move @@ -13,8 +13,8 @@ module pyth::price_info { friend pyth::pyth; - /// Sui Object version of PriceInfo. - /// Has a key and lives in global store. + /// Sui object version of PriceInfo. + /// Has a key ability, is unique for each price identifier, and lives in global store. struct PriceInfoObject has key, store { id: UID, price_info: PriceInfo From 29d1bc19ddbc3defdba71f8300fea494c4adbbf9 Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 11 Apr 2023 15:46:22 +0000 Subject: [PATCH 24/32] contract upgrades start --- .../sources/governance/contract_upgrade.move | 124 ++++++++- .../sui/contracts/sources/migrate.move | 67 +++++ .../contracts/sources/required_version.move | 241 ++++++++++++++++++ .../sui/contracts/sources/state.move | 140 +++++++++- .../contracts/sources/version_control.move | 51 ++++ 5 files changed, 613 insertions(+), 10 deletions(-) create mode 100644 target_chains/sui/contracts/sources/migrate.move create mode 100644 target_chains/sui/contracts/sources/required_version.move create mode 100644 target_chains/sui/contracts/sources/version_control.move diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move index e0130c1304..a25bd11bc1 100644 --- a/target_chains/sui/contracts/sources/governance/contract_upgrade.move +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -1,12 +1,128 @@ -module pyth::contract_upgrade { +// SPDX-License-Identifier: Apache 2 + +/// This module implements handling a governance VAA to enact upgrading the +/// Wormhole contract to a new build. The procedure to upgrade this contract +/// requires a Programmable Transaction, which includes the following procedure: +/// 1. Load new build. +/// 2. Authorize upgrade. +/// 3. Upgrade. +/// 4. Commit upgrade. +module wormhole::upgrade_contract { + use sui::clock::{Clock}; + use sui::event::{Self}; + use sui::object::{Self, ID}; + use sui::package::{UpgradeReceipt, UpgradeTicket}; + use pyth::state::{State}; use wormhole::state::{State as WormState}; + use wormhole::bytes32::{Self, Bytes32}; + use wormhole::consumed_vaas::{Self}; + use wormhole::cursor::{Self}; + use wormhole::state::{Self, State}; friend pyth::governance; - /// Payload should be the bytes digest of the new contract. - public(friend) fun execute(_worm_state: &WormState, _pyth_state: &State, _payload: vector){ - // TODO + /// Digest is all zeros. + const E_DIGEST_ZERO_BYTES: u64 = 0; + /// Specific governance payload ID (action) to complete upgrading the + /// contract. + const ACTION_UPGRADE_CONTRACT: u8 = 1; + + // Event reflecting package upgrade. + struct ContractUpgraded has drop, copy { + old_contract: ID, + new_contract: ID + } + + struct UpgradeContract { + digest: Bytes32 + } + + /// Redeem governance VAA to issue an `UpgradeTicket` for the upgrade given + /// a contract upgrade VAA. This governance message is only relevant for Sui + /// because a contract upgrade is only relevant to one particular network + /// (in this case Sui), whose build digest is encoded in this message. + /// + /// NOTE: This method is guarded by a minimum build version check. This + /// method could break backward compatibility on an upgrade. + public fun execute( + wormhole_state: &mut State, + vaa_buf: vector, + the_clock: &Clock + ): UpgradeTicket { + let msg = + governance_message::parse_and_verify_vaa( + wormhole_state, + vaa_buf, + the_clock + ); + + // Do not allow this VAA to be replayed. + consumed_vaas::consume( + state::borrow_mut_consumed_vaas(wormhole_state), + governance_message::vaa_hash(&msg) + ); + + // Proceed with processing new implementation version. + handle_upgrade_contract(wormhole_state, msg) + } + + /// Finalize the upgrade that ran to produce the given `receipt`. This + /// method invokes `state::commit_upgrade` which interacts with + /// `sui::package`. + public fun commit_upgrade( + self: &mut State, + receipt: UpgradeReceipt, + ) { + let latest_package_id = state::commit_upgrade(self, receipt); + + // Emit an event reflecting package ID change. + event::emit( + ContractUpgraded { + old_contract: object::id_from_address(@wormhole), + new_contract: latest_package_id + } + ); + } + + fun handle_upgrade_contract( + wormhole_state: &mut State, + msg: GovernanceMessage + ): UpgradeTicket { + // Verify that this governance message is to update the Wormhole fee. + let governance_payload = + governance_message::take_local_action( + msg, + state::governance_module(), + ACTION_UPGRADE_CONTRACT + ); + + // Deserialize the payload as amount to change the Wormhole fee. + let UpgradeContract { digest } = deserialize(governance_payload); + + state::authorize_upgrade(wormhole_state, digest) + } + + fun deserialize(payload: vector): UpgradeContract { + let cur = cursor::new(payload); + + // This amount cannot be greater than max u64. + let digest = bytes32::take_bytes(&mut cur); + assert!(bytes32::is_nonzero(&digest), E_DIGEST_ZERO_BYTES); + + cursor::destroy_empty(cur); + + UpgradeContract { digest } + } + + #[test_only] + public fun action(): u8 { + ACTION_UPGRADE_CONTRACT } } + +#[test_only] +module wormhole::upgrade_contract_tests { + // TODO +} \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/migrate.move b/target_chains/sui/contracts/sources/migrate.move new file mode 100644 index 0000000000..48f6df31b2 --- /dev/null +++ b/target_chains/sui/contracts/sources/migrate.move @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache 2 + +/// Note: This module is largely taken from the Sui Wormhole package: +/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/migrate.move + +/// This module implements an entry method intended to be called after an +/// upgrade has been commited. The purpose is to add one-off migration logic +/// that would alter pyth `State`. +/// +/// Included in migration is the ability to ensure that breaking changes for +/// any of pyth's methods by enforcing the current build version as their +/// required minimum version. +module pyth::migrate { + use pyth::state::{Self, State}; + + // This import is only used when `state::require_current_version` is used. + // use pytg::version_control::{Self as control}; + + /// Upgrade procedure is not complete (most likely due to an upgrade not + /// being initialized since upgrades can only be performed via programmable + /// transaction). + const E_CANNOT_MIGRATE: u64 = 0; + + /// Execute migration logic. See `pyth::migrate` description for more + /// info. + public entry fun migrate(pyth_state: &mut State) { + // pyth `State` only allows one to call `migrate` after the upgrade + // procedure completed. + assert!(state::can_migrate(pyth_state), E_CANNOT_MIGRATE); + + //////////////////////////////////////////////////////////////////////// + // + // If there are any methods that require the current build, we need to + // explicity require them here. + // + // Calls to `require_current_version` are commented out for convenience. + // + //////////////////////////////////////////////////////////////////////// + + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + + //////////////////////////////////////////////////////////////////////// + // + // NOTE: Put any one-off migration logic here. + // + // Most upgrades likely won't need to do anything, in which case the + // rest of this function's body may be empty. Make sure to delete it + // after the migration has gone through successfully. + // + // WARNING: The migration does *not* proceed atomically with the + // upgrade (as they are done in separate transactions). + // If the nature of your migration absolutely requires the migration to + // happen before certain other functionality is available, then guard + // that functionality with the `assert!` from above. + // + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // Ensure that `migrate` cannot be called again. + state::disable_migration(pyth_state); + } +} diff --git a/target_chains/sui/contracts/sources/required_version.move b/target_chains/sui/contracts/sources/required_version.move new file mode 100644 index 0000000000..4528c5d076 --- /dev/null +++ b/target_chains/sui/contracts/sources/required_version.move @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache 2 + +/// Note: This module is based on the required_version module +/// from the Sui Wormhole package: +/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/resources/required_version.move + +/// This module implements a mechanism for version control. While keeping track +/// of the latest version of a package build, `RequiredVersion` manages the +/// minimum required version number for any method in that package. For any +/// upgrade where a particular method can have backward compatibility, the +/// minimum version would not have to change (because the method should work the +/// same way with the previous version or current version). +/// +/// If there happens to be a breaking change for a particular method, this +/// module can force that the method's minimum requirement be the latest build. +/// If a previous build were used, the method would abort if a check is in place +/// with `RequiredVersion`. +/// +/// There is no magic behind the way ths module works. `RequiredVersion` is +/// intended to live in a package's shared object that gets passed into its +/// methods (e.g. pyth's `State` object). +module pyth::required_version { + use sui::dynamic_field::{Self as field}; + use sui::object::{Self, UID}; + use sui::package::{Self, UpgradeCap}; + use sui::tx_context::{TxContext}; + + /// Build version passed does not meet method's minimum required version. + const E_OUTDATED_VERSION: u64 = 0; + + /// Container to keep track of latest build version. Dynamic fields are + /// associated with its `id`. + struct RequiredVersion has store { + id: UID, + latest_version: u64 + } + + struct Key has store, drop, copy {} + + /// Create new `RequiredVersion` with a configured starting version. + public fun new(version: u64, ctx: &mut TxContext): RequiredVersion { + RequiredVersion { + id: object::new(ctx), + latest_version: version + } + } + + /// Retrieve latest build version. + public fun current(self: &RequiredVersion): u64 { + self.latest_version + } + + /// Add specific method handling via custom `MethodType`. At the time a + /// method is added, the minimum build version associated with this method + /// by default is the latest version. + public fun add(self: &mut RequiredVersion) { + field::add(&mut self.id, Key {}, self.latest_version) + } + + /// This method will abort if the version for a particular `MethodType` is + /// not up-to-date with the version of the current build. + /// + /// For example, if the minimum requirement for `foobar` module (with an + /// appropriately named `MethodType` like `FooBar`) is `1` and the current + /// implementation version is `2`, this method will succeed because the + /// build meets the minimum required version of `1` in order for `foobar` to + /// work. So if someone were to use an older build like version `1`, this + /// method will succeed. + /// + /// But if `check_minimum_requirement` were invoked for `foobar` when the + /// minimum requirement is `2` and the current build is only version `1`, + /// then this method will abort because the build does not meet the minimum + /// version requirement for `foobar`. + /// + /// This method also assumes that the `MethodType` being checked for is + /// already a dynamic field (using `add`) during initialization. + public fun check_minimum_requirement( + self: &RequiredVersion, + build_version: u64 + ) { + assert!( + build_version >= minimum_for(self), + E_OUTDATED_VERSION + ); + } + + /// At `commit_upgrade`, use this method to update the tracker's knowledge + /// of the latest upgrade (build) version, which is obtained from the + /// `UpgradeCap` in `sui::package`. + public fun update_latest( + self: &mut RequiredVersion, + upgrade_cap: &UpgradeCap + ) { + self.latest_version = package::version(upgrade_cap); + } + + /// Once the global version is updated via `commit_upgrade` and there is a + /// particular method that has a breaking change, use this method to uptick + /// that method's minimum required version to the latest. + public fun require_current_version(self: &mut RequiredVersion) { + let min_version = field::borrow_mut(&mut self.id, Key {}); + *min_version = self.latest_version; + } + + /// Retrieve the minimum required version for a particular method (via + /// `MethodType`). + public fun minimum_for(self: &RequiredVersion): u64 { + *field::borrow(&self.id, Key {}) + } + + #[test_only] + public fun set_required_version( + self: &mut RequiredVersion, + version: u64 + ) { + *field::borrow_mut(&mut self.id, Key {}) = version; + } + + #[test_only] + public fun destroy(req: RequiredVersion) { + let RequiredVersion { id, latest_version: _} = req; + object::delete(id); + } +} + +#[test_only] +module pyth::required_version_test { + use sui::hash::{keccak256}; + use sui::object::{Self}; + use sui::package::{Self}; + use sui::tx_context::{Self}; + + use pyth::required_version::{Self}; + + struct SomeMethod {} + struct AnotherMethod {} + + #[test] + public fun test_check_minimum_requirement() { + let ctx = &mut tx_context::dummy(); + + let version = 1; + let req = required_version::new(version, ctx); + assert!(required_version::current(&req) == version, 0); + + required_version::add(&mut req); + assert!(required_version::minimum_for(&req) == version, 0); + + // Should not abort here. + required_version::check_minimum_requirement(&req, version); + + // And should not abort if the version is anything greater than the + // current. + let new_version = version + 1; + required_version::check_minimum_requirement( + &req, + new_version + ); + + // Uptick based on new upgrade. + let upgrade_cap = package::test_publish( + object::id_from_address(@pyth), + ctx + ); + let digest = keccak256(&x"DEADBEEF"); + let policy = package::upgrade_policy(&upgrade_cap); + let upgrade_ticket = + package::authorize_upgrade(&mut upgrade_cap, policy, digest); + let upgrade_receipt = package::test_upgrade(upgrade_ticket); + package::commit_upgrade(&mut upgrade_cap, upgrade_receipt); + assert!(package::version(&upgrade_cap) == new_version, 0); + + // Update to the latest version. + required_version::update_latest(&mut req, &upgrade_cap); + assert!(required_version::current(&req) == new_version, 0); + + // Should still not abort here. + required_version::check_minimum_requirement( + &req, + new_version + ); + + // Require new version for `SomeMethod` and show that + // `check_minimum_requirement` still succeeds. + required_version::require_current_version(&mut req); + assert!( + required_version::minimum_for(&req) == new_version, + 0 + ); + required_version::check_minimum_requirement( + &req, + new_version + ); + + // If another method gets added to the mix, it should automatically meet + // the minimum requirement because its version will be the latest. + required_version::add(&mut req); + assert!( + required_version::minimum_for(&req) == new_version, + 0 + ); + required_version::check_minimum_requirement( + &req, + new_version + ); + + // Clean up. + package::make_immutable(upgrade_cap); + required_version::destroy(req); + } + + #[test] + #[expected_failure(abort_code = required_version::E_OUTDATED_VERSION)] + public fun test_cannot_check_minimum_requirement_with_outdated_version() { + let ctx = &mut tx_context::dummy(); + + let version = 1; + let req = required_version::new(version, ctx); + assert!(required_version::current(&req) == version, 0); + + required_version::add(&mut req); + + // Should not abort here. + required_version::check_minimum_requirement(&req, version); + + // Uptick minimum requirement and fail at `check_minimum_requirement`. + let new_version = 10; + required_version::set_required_version( + &mut req, + new_version + ); + let old_version = new_version - 1; + required_version::check_minimum_requirement( + &req, + old_version + ); + + // Clean up. + required_version::destroy(req); + } +} diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index ff9a7c0bfb..13d6763c0f 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -3,13 +3,18 @@ module pyth::state { use sui::object::{Self, UID, ID}; use sui::transfer::{Self}; use sui::tx_context::{Self, TxContext}; - use sui::package::{Self, UpgradeCap}; + use sui::dynamic_field::{Self as field}; + use sui::package::{Self, UpgradeCap, UpgradeReceipt, UpgradeTicket}; use pyth::data_source::{Self, DataSource}; use pyth::price_info::{Self}; use pyth::price_identifier::{PriceIdentifier}; + use pyth::required_version::{Self, RequiredVersion}; + use wormhole::version_control::{Self as control}; use wormhole::setup::{assert_package_upgrade_cap}; + use wormhole::consumed_vaas::{Self}; + use wormhole::bytes32::{Self, Bytes32}; friend pyth::pyth; friend pyth::pyth_tests; @@ -19,6 +24,11 @@ module pyth::state { friend pyth::set_data_sources; friend pyth::governance; friend pyth::set_governance_data_source; + friend pyth::migrate; + + const E_BUILD_VERSION_MISMATCH: u64 = 0; + const E_INVALID_BUILD_VERSION: u64 = 1; + const E_VAA_ALREADY_CONSUMED: u64 = 2; /// Capability for creating a bridge state object, granted to sender when this /// module is deployed @@ -26,13 +36,27 @@ module pyth::state { id: UID } + /// Used as key for dynamic field reflecting whether `migrate` can be + /// called. + /// + /// See `migrate` module for more info. + struct MigrationControl has store, drop, copy {} + + /// Used as key for dynamic field for consumed VAAs + struct ConsumedVAAsKey has store, drop, copy {} + struct State has key { id: UID, governance_data_source: DataSource, last_executed_governance_sequence: u64, stale_price_threshold: u64, base_update_fee: u64, - upgrade_cap: UpgradeCap + + // Upgrade capability. + upgrade_cap: UpgradeCap, + + /// Contract build version tracker. + required_version: RequiredVersion } fun init(ctx: &mut TxContext) { @@ -56,7 +80,7 @@ module pyth::state { ); } - // Initialization + /// Initialization public(friend) fun init_and_share_state( deployer: DeployerCap, upgrade_cap: UpgradeCap, @@ -66,9 +90,9 @@ module pyth::state { sources: vector, ctx: &mut TxContext ) { - // TODO - version control - // let version = wormhole::version_control::version(); - //assert!(version == 1, E_INVALID_BUILD_VERSION); + // Only init and share state once (in the initial deployment). + let version = wormhole::version_control::version(); + assert!(version == 1, E_INVALID_BUILD_VERSION); let DeployerCap { id } = deployer; object::delete(id); @@ -81,6 +105,9 @@ module pyth::state { let uid = object::new(ctx); + field::add(&mut uid, MigrationControl {}, false); + field::add(&mut uid, ConsumedVAAsKey {}, consumed_vaas::new(ctx)); + // Create a set that contains all registered data sources and // attach it to uid as a dynamic field to minimize the // size of State. @@ -106,10 +133,111 @@ module pyth::state { last_executed_governance_sequence: 0, stale_price_threshold, base_update_fee, + required_version: required_version::new(control::version(), ctx) } ); } + /// Retrieve current build version of latest upgrade. + public fun current_version(self: &State): u64 { + required_version::current(&self.required_version) + } + + /// Issue an `UpgradeTicket` for the upgrade. + public(friend) fun authorize_upgrade( + self: &mut State, + implementation_digest: Bytes32 + ): UpgradeTicket { + let policy = package::upgrade_policy(&self.upgrade_cap); + + // TODO: grab package ID from `UpgradeCap` and store it + // in a dynamic field. This will be the old ID after the upgrade. + // Both IDs will be emitted in a `ContractUpgraded` event. + package::authorize_upgrade( + &mut self.upgrade_cap, + policy, + bytes32::to_bytes(implementation_digest), + ) + } + + /// Finalize the upgrade that ran to produce the given `receipt`. + public(friend) fun commit_upgrade( + self: &mut State, + receipt: UpgradeReceipt + ): ID { + // Uptick the upgrade cap version number using this receipt. + package::commit_upgrade(&mut self.upgrade_cap, receipt); + + // Check that the upticked hard-coded version version agrees with the + // upticked version number. + assert!( + package::version(&self.upgrade_cap) == control::version() + 1, + E_BUILD_VERSION_MISMATCH + ); + + // Update global version. + required_version::update_latest( + &mut self.required_version, + &self.upgrade_cap + ); + + // Enable `migrate` to be called after commiting the upgrade. + // + // A separate method is required because `state` is a dependency of + // `migrate`. This method warehouses state modifications required + // for the new implementation plus enabling any methods required to be + // gated by the current implementation version. In most cases `migrate` + // is a no-op. But it still must be called in order to reset the + // migration control to `false`. + // + // See `migrate` module for more info. + enable_migration(self); + + package::upgrade_package(&self.upgrade_cap) + } + + /// Enforce a particular method to use the current build version as its + /// minimum required version. This method ensures that a method is not + /// backwards compatible with older builds. + public(friend) fun require_current_version(self: &mut State) { + required_version::require_current_version( + &mut self.required_version, + ) + } + + /// Check whether a particular method meets the minimum build version for + /// the latest Wormhole implementation. + public(friend) fun check_minimum_requirement(self: &State) { + required_version::check_minimum_requirement( + &self.required_version, + control::version() + ) + } + + // Upgrade and migration-related functionality + + /// Check whether `migrate` can be called. + /// + /// See `wormhole::migrate` module for more info. + public fun can_migrate(self: &State): bool { + *field::borrow(&self.id, MigrationControl {}) + } + + /// Allow `migrate` to be called after upgrade. + /// + /// See `wormhole::migrate` module for more info. + public(friend) fun enable_migration(self: &mut State) { + *field::borrow_mut(&mut self.id, MigrationControl {}) = true; + } + + /// Disallow `migrate` to be called. + /// + /// See `wormhole::migrate` module for more info. + public(friend) fun disable_migration(self: &mut State) { + *field::borrow_mut(&mut self.id, MigrationControl {}) = false; + } + + // Accessors public fun get_stale_price_threshold_secs(s: &State): u64 { s.stale_price_threshold diff --git a/target_chains/sui/contracts/sources/version_control.move b/target_chains/sui/contracts/sources/version_control.move new file mode 100644 index 0000000000..974c7b8f05 --- /dev/null +++ b/target_chains/sui/contracts/sources/version_control.move @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache 2 + +/// Note: This module is based on the version_control module in +/// the Sui Wormhole package: +/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/version_control.move + +/// This module implements dynamic field keys as empty structs. These keys with +/// `RequiredVersion` are used to determine minimum build requirements for +/// particular Pyth methods and breaking backward compatibility for these +/// methods if an upgrade requires the latest upgrade version for its +/// functionality. +/// +/// See `pyth::required_version` and `pyth::state` for more info. +module pyth::version_control { + /// This value tracks the current version of the Wormhole version. We are + /// placing this constant value at the top, which goes against Move style + /// guides so that we bring special attention to changing this value when + /// a new implementation is built for a contract upgrade. + const CURRENT_BUILD_VERSION: u64 = 1; + + /// Key used to check minimum version requirement for `emitter` module. + struct Emitter {} + + /// Key used to check minimum version requirement for `governance_message` + /// module. + struct GovernanceMessage {} + + /// Key used to check minimum version requirement for `publish_module` + /// module. + struct PublishMessage {} + + /// Key used to check minimum version requirement for `set_fee` module. + struct SetFee {} + + /// Key used to check minimum version requirement for `transfer_fee` module. + struct TransferFee {} + + /// Key used to check minimum version requirement for `update_guardian_set` + /// module. + struct UpdateGuardianSet {} + + /// Key used to check minimum version requirement for `vaa` module. + struct Vaa {} + + /// Return const value `CURRENT_BUILD_VERSION` for this particular build. + /// This value is used to determine whether this implementation meets + /// minimum requirements for various Pyth methods required by `State`. + public fun version(): u64 { + CURRENT_BUILD_VERSION + } +} From acacf1704fdae5eb479b2747f1269ff3e90ca598 Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 11 Apr 2023 22:29:47 +0000 Subject: [PATCH 25/32] contract upgradeability --- .../sources/governance/contract_upgrade.move | 61 +++++----------- .../sources/governance/governance.move | 52 ++++++++++---- .../sources/governance/governance_action.move | 5 ++ .../sources/governance/set_data_sources.move | 11 +-- .../set_governance_data_source.move | 14 ++-- .../governance/set_stale_price_threshold.move | 11 +-- .../sources/governance/set_update_fee.move | 11 +-- .../sources/governance/transfer_fee.move | 51 ++++++++++++++ .../sui/contracts/sources/migrate.move | 12 ++-- target_chains/sui/contracts/sources/pyth.move | 23 ++++--- .../sui/contracts/sources/state.move | 69 ++++++++++++++----- .../contracts/sources/version_control.move | 31 ++++----- 12 files changed, 229 insertions(+), 122 deletions(-) create mode 100644 target_chains/sui/contracts/sources/governance/transfer_fee.move diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move index a25bd11bc1..6b9453861f 100644 --- a/target_chains/sui/contracts/sources/governance/contract_upgrade.move +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -1,25 +1,26 @@ // SPDX-License-Identifier: Apache 2 +/// Note: This module is based on the upgrade_contract module +/// from the Sui Wormhole package: +/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/governance/upgrade_contract.move + /// This module implements handling a governance VAA to enact upgrading the -/// Wormhole contract to a new build. The procedure to upgrade this contract +/// Pyth contract to a new build. The procedure to upgrade this contract /// requires a Programmable Transaction, which includes the following procedure: /// 1. Load new build. /// 2. Authorize upgrade. /// 3. Upgrade. /// 4. Commit upgrade. -module wormhole::upgrade_contract { +module pyth::contract_upgrade { use sui::clock::{Clock}; use sui::event::{Self}; use sui::object::{Self, ID}; use sui::package::{UpgradeReceipt, UpgradeTicket}; - use pyth::state::{State}; + use pyth::state::{Self, State}; - use wormhole::state::{State as WormState}; use wormhole::bytes32::{Self, Bytes32}; - use wormhole::consumed_vaas::{Self}; use wormhole::cursor::{Self}; - use wormhole::state::{Self, State}; friend pyth::governance; @@ -47,25 +48,12 @@ module wormhole::upgrade_contract { /// NOTE: This method is guarded by a minimum build version check. This /// method could break backward compatibility on an upgrade. public fun execute( - wormhole_state: &mut State, - vaa_buf: vector, - the_clock: &Clock + pyth_state: &mut State, + payload: vector, + _the_clock: &Clock ): UpgradeTicket { - let msg = - governance_message::parse_and_verify_vaa( - wormhole_state, - vaa_buf, - the_clock - ); - - // Do not allow this VAA to be replayed. - consumed_vaas::consume( - state::borrow_mut_consumed_vaas(wormhole_state), - governance_message::vaa_hash(&msg) - ); - // Proceed with processing new implementation version. - handle_upgrade_contract(wormhole_state, msg) + handle_upgrade_contract(pyth_state, payload) } /// Finalize the upgrade that ran to produce the given `receipt`. This @@ -80,28 +68,20 @@ module wormhole::upgrade_contract { // Emit an event reflecting package ID change. event::emit( ContractUpgraded { - old_contract: object::id_from_address(@wormhole), + old_contract: object::id_from_address(@pyth), new_contract: latest_package_id } ); } fun handle_upgrade_contract( - wormhole_state: &mut State, - msg: GovernanceMessage + pyth_state: &mut State, + payload: vector ): UpgradeTicket { - // Verify that this governance message is to update the Wormhole fee. - let governance_payload = - governance_message::take_local_action( - msg, - state::governance_module(), - ACTION_UPGRADE_CONTRACT - ); - - // Deserialize the payload as amount to change the Wormhole fee. - let UpgradeContract { digest } = deserialize(governance_payload); - - state::authorize_upgrade(wormhole_state, digest) + + let UpgradeContract { digest } = deserialize(payload); + + state::authorize_upgrade(pyth_state, digest) } fun deserialize(payload: vector): UpgradeContract { @@ -121,8 +101,3 @@ module wormhole::upgrade_contract { ACTION_UPGRADE_CONTRACT } } - -#[test_only] -module wormhole::upgrade_contract_tests { - // TODO -} \ No newline at end of file diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move index d84657a03d..1198e70fdc 100644 --- a/target_chains/sui/contracts/sources/governance/governance.move +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -1,5 +1,7 @@ module pyth::governance { use sui::clock::{Clock}; + use sui::package::{UpgradeTicket}; + use sui::tx_context::{TxContext}; use pyth::data_source::{Self}; use pyth::governance_instruction; @@ -8,6 +10,7 @@ module pyth::governance { use pyth::set_governance_data_source; use pyth::set_data_sources; use pyth::set_stale_price_threshold; + use pyth::transfer_fee; use pyth::state::{State}; use pyth::set_update_fee; use pyth::state; @@ -18,23 +21,47 @@ module pyth::governance { const E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO: u64 = 0; const E_INVALID_GOVERNANCE_ACTION: u64 = 1; const E_INVALID_GOVERNANCE_DATA_SOURCE: u64 = 2; - const E_INVALID_GOVERNANCE_SEQUENCE_NUMBER: u64 = 3; + const E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE: u64 = 3; + const E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE: u64 = 4; - public entry fun execute_governance_instruction( + /// Rather than having execute_governance_instruction handle the contract + /// upgrade governance instruction, we have a separate function that processes + /// contract upgrade instructions, because doing contract upgrades is a + /// multi-step process, and the first step of doing a contract upgrade + /// yields a return value, namely the upgrade ticket, which is non-droppable. + public fun execute_contract_upgrade_governance_instruction( pyth_state : &mut State, worm_state: &WormState, vaa_bytes: vector, clock: &Clock + ): UpgradeTicket { + let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock); + let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa)); + let action = governance_instruction::get_action(&instruction); + assert!(action == governance_action::new_contract_upgrade(), + E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE); + assert!(governance_instruction::get_target_chain_id(&instruction) != 0, + E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO); + contract_upgrade::execute(pyth_state, governance_instruction::destroy(instruction), clock) + } + + /// Execute a governance instruction. + public entry fun execute_governance_instruction( + pyth_state : &mut State, + worm_state: &WormState, + vaa_bytes: vector, + clock: &Clock, + ctx: &mut TxContext ) { - let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, clock); + let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock); let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa)); - // Dispatch the instruction to the appropiate handler + // Get the governance action. let action = governance_instruction::get_action(&instruction); + + // Dispatch the instruction to the appropiate handler. if (action == governance_action::new_contract_upgrade()) { - assert!(governance_instruction::get_target_chain_id(&instruction) != 0, - E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO); - contract_upgrade::execute(worm_state, pyth_state, governance_instruction::destroy(instruction)); + abort(E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE) } else if (action == governance_action::new_set_governance_data_source()) { set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction)); } else if (action == governance_action::new_set_data_sources()) { @@ -43,13 +70,15 @@ module pyth::governance { set_update_fee::execute(pyth_state, governance_instruction::destroy(instruction)); } else if (action == governance_action::new_set_stale_price_threshold()) { set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_transfer_fee()) { + transfer_fee::execute(pyth_state, governance_instruction::destroy(instruction), ctx); } else { governance_instruction::destroy(instruction); assert!(false, E_INVALID_GOVERNANCE_ACTION); } } - fun parse_and_verify_governance_vaa( + fun parse_and_verify_and_replay_protect_governance_vaa( pyth_state: &mut State, worm_state: &WormState, bytes: vector, @@ -66,11 +95,8 @@ module pyth::governance { vaa::emitter_address(&parsed_vaa))), E_INVALID_GOVERNANCE_DATA_SOURCE); - // Check that the sequence number is greater than the last executed governance VAA - let sequence = vaa::sequence(&parsed_vaa); - assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), E_INVALID_GOVERNANCE_SEQUENCE_NUMBER); - state::set_last_executed_governance_sequence(pyth_state, sequence); - + // Prevent replay attacks by consuming the VAA digest (adding it to a set) + state::consume_vaa(pyth_state, vaa::digest(&parsed_vaa)); parsed_vaa } } diff --git a/target_chains/sui/contracts/sources/governance/governance_action.move b/target_chains/sui/contracts/sources/governance/governance_action.move index efd79735ce..762060eb23 100644 --- a/target_chains/sui/contracts/sources/governance/governance_action.move +++ b/target_chains/sui/contracts/sources/governance/governance_action.move @@ -6,6 +6,7 @@ module pyth::governance_action { const SET_DATA_SOURCES: u8 = 2; const SET_UPDATE_FEE: u8 = 3; const SET_STALE_PRICE_THRESHOLD: u8 = 4; + const TRANSFER_FEE: u8 = 5; const E_INVALID_GOVERNANCE_ACTION: u64 = 5; @@ -37,4 +38,8 @@ module pyth::governance_action { public fun new_set_stale_price_threshold(): GovernanceAction { GovernanceAction { value: SET_STALE_PRICE_THRESHOLD } } + + public fun new_transfer_fee(): GovernanceAction { + GovernanceAction { value: TRANSFER_FEE } + } } diff --git a/target_chains/sui/contracts/sources/governance/set_data_sources.move b/target_chains/sui/contracts/sources/governance/set_data_sources.move index f22969bccf..3815da5c09 100644 --- a/target_chains/sui/contracts/sources/governance/set_data_sources.move +++ b/target_chains/sui/contracts/sources/governance/set_data_sources.move @@ -8,19 +8,22 @@ module pyth::set_data_sources { use pyth::deserialize; use pyth::data_source::{Self, DataSource}; use pyth::state::{Self, State}; + use pyth::version_control::{SetDataSources}; friend pyth::governance; - struct SetDataSources { + struct DataSources { sources: vector, } public(friend) fun execute(state: &mut State, payload: vector) { - let SetDataSources { sources } = from_byte_vec(payload); + state::check_minimum_requirement(state); + + let DataSources { sources } = from_byte_vec(payload); state::set_data_sources(state, sources); } - fun from_byte_vec(bytes: vector): SetDataSources { + fun from_byte_vec(bytes: vector): DataSources { let cursor = cursor::new(bytes); let data_sources_count = deserialize::deserialize_u8(&mut cursor); @@ -37,7 +40,7 @@ module pyth::set_data_sources { cursor::destroy_empty(cursor); - SetDataSources { + DataSources { sources } } diff --git a/target_chains/sui/contracts/sources/governance/set_governance_data_source.move b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move index 4616339b65..a09a6093d4 100644 --- a/target_chains/sui/contracts/sources/governance/set_governance_data_source.move +++ b/target_chains/sui/contracts/sources/governance/set_governance_data_source.move @@ -2,33 +2,35 @@ module pyth::set_governance_data_source { use pyth::deserialize; use pyth::data_source; use pyth::state::{Self, State}; + use pyth::version_control::SetGovernanceDataSource; use wormhole::cursor; use wormhole::external_address::{Self, ExternalAddress}; use wormhole::bytes32::{Self}; - //use wormhole::state::{Self} friend pyth::governance; - struct SetGovernanceDataSource { + struct GovernanceDataSource { emitter_chain_id: u64, emitter_address: ExternalAddress, initial_sequence: u64, } public(friend) fun execute(pyth_state: &mut State, payload: vector) { - let SetGovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence } = from_byte_vec(payload); + state::check_minimum_requirement(pyth_state); + + // TODO - What is GovernanceDataSource initial_sequence used for? + let GovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence: _initial_sequence } = from_byte_vec(payload); state::set_governance_data_source(pyth_state, data_source::new(emitter_chain_id, emitter_address)); - state::set_last_executed_governance_sequence(pyth_state, initial_sequence); } - fun from_byte_vec(bytes: vector): SetGovernanceDataSource { + fun from_byte_vec(bytes: vector): GovernanceDataSource { let cursor = cursor::new(bytes); let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32))); let initial_sequence = deserialize::deserialize_u64(&mut cursor); cursor::destroy_empty(cursor); - SetGovernanceDataSource { + GovernanceDataSource { emitter_chain_id: (emitter_chain_id as u64), emitter_address, initial_sequence diff --git a/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move b/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move index 6fdb6a2ed0..7ec7da3b4c 100644 --- a/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move +++ b/target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move @@ -2,23 +2,26 @@ module pyth::set_stale_price_threshold { use wormhole::cursor; use pyth::deserialize; use pyth::state::{Self, State}; + use pyth::version_control::SetStalePriceThreshold; friend pyth::governance; - struct SetStalePriceThreshold { + struct StalePriceThreshold { threshold: u64, } public(friend) fun execute(state: &mut State, payload: vector) { - let SetStalePriceThreshold { threshold } = from_byte_vec(payload); + state::check_minimum_requirement(state); + + let StalePriceThreshold { threshold } = from_byte_vec(payload); state::set_stale_price_threshold_secs(state, threshold); } - fun from_byte_vec(bytes: vector): SetStalePriceThreshold { + fun from_byte_vec(bytes: vector): StalePriceThreshold { let cursor = cursor::new(bytes); let threshold = deserialize::deserialize_u64(&mut cursor); cursor::destroy_empty(cursor); - SetStalePriceThreshold { + StalePriceThreshold { threshold } } diff --git a/target_chains/sui/contracts/sources/governance/set_update_fee.move b/target_chains/sui/contracts/sources/governance/set_update_fee.move index 0286fc07d2..6c339b469f 100644 --- a/target_chains/sui/contracts/sources/governance/set_update_fee.move +++ b/target_chains/sui/contracts/sources/governance/set_update_fee.move @@ -3,6 +3,7 @@ module pyth::set_update_fee { use pyth::deserialize; use pyth::state::{Self, State}; + use pyth::version_control::SetUpdateFee; use wormhole::cursor; @@ -12,24 +13,26 @@ module pyth::set_update_fee { const MAX_U64: u128 = (1 << 64) - 1; const E_EXPONENT_DOES_NOT_FIT_IN_U8: u64 = 0; - struct SetUpdateFee { + struct UpdateFee { mantissa: u64, exponent: u64, } public(friend) fun execute(pyth_state: &mut State, payload: vector) { - let SetUpdateFee { mantissa, exponent } = from_byte_vec(payload); + state::check_minimum_requirement(pyth_state); + + let UpdateFee { mantissa, exponent } = from_byte_vec(payload); assert!(exponent <= 255, E_EXPONENT_DOES_NOT_FIT_IN_U8); let fee = apply_exponent(mantissa, (exponent as u8)); state::set_base_update_fee(pyth_state, fee); } - fun from_byte_vec(bytes: vector): SetUpdateFee { + fun from_byte_vec(bytes: vector): UpdateFee { let cursor = cursor::new(bytes); let mantissa = deserialize::deserialize_u64(&mut cursor); let exponent = deserialize::deserialize_u64(&mut cursor); cursor::destroy_empty(cursor); - SetUpdateFee { + UpdateFee { mantissa, exponent, } diff --git a/target_chains/sui/contracts/sources/governance/transfer_fee.move b/target_chains/sui/contracts/sources/governance/transfer_fee.move new file mode 100644 index 0000000000..f9361e539b --- /dev/null +++ b/target_chains/sui/contracts/sources/governance/transfer_fee.move @@ -0,0 +1,51 @@ +module pyth::transfer_fee { + + use sui::transfer::Self; + use sui::coin::Self; + use sui::tx_context::TxContext; + + use wormhole::cursor; + use wormhole::external_address::{Self}; + use wormhole::bytes32::{Self}; + + use pyth::state::{Self, State}; + use pyth::version_control::{TransferFee}; + + friend pyth::governance; + + struct PythFee { + amount: u64, + recipient: address + } + + public(friend) fun execute(state: &mut State, payload: vector, ctx: &mut TxContext) { + state::check_minimum_requirement(state); + + let PythFee { amount, recipient } = from_byte_vec(payload); + + transfer::public_transfer( + coin::from_balance( + state::withdraw_fee(state, amount), + ctx + ), + recipient + ); + } + + fun from_byte_vec(payload: vector): PythFee { + let cur = cursor::new(payload); + + // This amount cannot be greater than max u64. + let amount = bytes32::to_u64_be(bytes32::take_bytes(&mut cur)); + + // Recipient must be non-zero address. + let recipient = external_address::take_nonzero(&mut cur); + + cursor::destroy_empty(cur); + + PythFee { + amount: (amount as u64), + recipient: external_address::to_address(recipient) + } + } +} diff --git a/target_chains/sui/contracts/sources/migrate.move b/target_chains/sui/contracts/sources/migrate.move index 48f6df31b2..55b519ffe5 100644 --- a/target_chains/sui/contracts/sources/migrate.move +++ b/target_chains/sui/contracts/sources/migrate.move @@ -37,12 +37,14 @@ module pyth::migrate { // //////////////////////////////////////////////////////////////////////// - // state::require_current_version(pyth_state); - // state::require_current_version(pyth_state); - // state::require_current_version(pyth_state); - // state::require_current_version(pyth_state); + + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); // state::require_current_version(pyth_state); - // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); + // state::require_current_version(pyth_state); //////////////////////////////////////////////////////////////////////// // diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 898c32739f..fefe00f3e1 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -15,6 +15,7 @@ module pyth::pyth { use pyth::price_feed::{Self}; use pyth::price::{Self, Price}; use pyth::price_identifier::{PriceIdentifier}; + use pyth::version_control::{UpdatePriceFeeds, CreatePriceFeeds}; use wormhole::external_address::{Self}; use wormhole::vaa::{Self}; @@ -93,6 +94,8 @@ module pyth::pyth { clock: &Clock, ctx: &mut TxContext ){ + // Version control. + state::check_minimum_requirement(pyth_state); while (!vector::is_empty(&vaas)) { let vaa = vector::pop_back(&mut vaas); @@ -137,8 +140,8 @@ module pyth::pyth { }; } - /// Update PriceInfo objects and corresponding price feeds with the - /// data in the given VAAs. + /// Update Pyth Price Info objects (containing price feeds) with the + /// price data in the given VAAs. /// /// The vaas argument is a vector of VAAs encoded as bytes. /// @@ -158,6 +161,9 @@ module pyth::pyth { fee: Coin, clock: &Clock ){ + // Version control. + state::check_minimum_requirement(pyth_state); + // Charge the message update fee assert!(get_total_update_fee(pyth_state, &vaas) <= coin::value(&fee), E_INSUFFICIENT_FEE); @@ -176,9 +182,11 @@ module pyth::pyth { }; } - /// Precondition: A Sui object of type PriceInfoObject must exist for each update + /// Make sure that a Sui object of type PriceInfoObject exists for each update /// encoded in the worm_vaa (batch_attestation_vaa). These should be passed in - /// via the price_info_objects argument. + /// via the price_info_objects argument. If for any price feed update, a + /// a PriceInfoObject with a matching price identifier is not found, the update_cache + /// function will revert, causing this function to revert. fun update_price_feed_from_single_vaa( worm_state: &WormState, pyth_state: &PythState, @@ -216,10 +224,9 @@ module pyth::pyth { let update = vector::pop_back(&mut updates); let i = 0; let found = false; - // Find PriceInfoObjects corresponding to the current update (PriceInfo). - // TODO - Construct an in-memory table to make look-ups faster? - // This loop might be expensive if there are a large number - // of updates and/or price_info_objects we are updating. + // Note - Would it be worth it to construct an in-memory hash-map to make look-ups faster? + // This loop might be expensive if there are a large number of price_info_objects + // passed in. while (i < vector::length(price_info_objects) && found == false){ // Check if the current price info object corresponds to the price feed that // the update is meant for. diff --git a/target_chains/sui/contracts/sources/state.move b/target_chains/sui/contracts/sources/state.move index 13d6763c0f..51a69132c7 100644 --- a/target_chains/sui/contracts/sources/state.move +++ b/target_chains/sui/contracts/sources/state.move @@ -5,16 +5,19 @@ module pyth::state { use sui::tx_context::{Self, TxContext}; use sui::dynamic_field::{Self as field}; use sui::package::{Self, UpgradeCap, UpgradeReceipt, UpgradeTicket}; + use sui::balance::{Balance}; + use sui::sui::SUI; use pyth::data_source::{Self, DataSource}; use pyth::price_info::{Self}; use pyth::price_identifier::{PriceIdentifier}; use pyth::required_version::{Self, RequiredVersion}; - use wormhole::version_control::{Self as control}; + use pyth::version_control::{Self as control}; use wormhole::setup::{assert_package_upgrade_cap}; - use wormhole::consumed_vaas::{Self}; + use wormhole::consumed_vaas::{Self, ConsumedVAAs}; use wormhole::bytes32::{Self, Bytes32}; + use wormhole::fee_collector::{Self, FeeCollector}; friend pyth::pyth; friend pyth::pyth_tests; @@ -25,6 +28,8 @@ module pyth::state { friend pyth::governance; friend pyth::set_governance_data_source; friend pyth::migrate; + friend pyth::contract_upgrade; + friend pyth::transfer_fee; const E_BUILD_VERSION_MISMATCH: u64 = 0; const E_INVALID_BUILD_VERSION: u64 = 1; @@ -42,19 +47,19 @@ module pyth::state { /// See `migrate` module for more info. struct MigrationControl has store, drop, copy {} - /// Used as key for dynamic field for consumed VAAs - struct ConsumedVAAsKey has store, drop, copy {} - struct State has key { id: UID, governance_data_source: DataSource, - last_executed_governance_sequence: u64, stale_price_threshold: u64, base_update_fee: u64, + consumed_vaas: ConsumedVAAs, // Upgrade capability. upgrade_cap: UpgradeCap, + // Fee collector. + fee_collector: FeeCollector, + /// Contract build version tracker. required_version: RequiredVersion } @@ -106,7 +111,6 @@ module pyth::state { let uid = object::new(ctx); field::add(&mut uid, MigrationControl {}, false); - field::add(&mut uid, ConsumedVAAsKey {}, consumed_vaas::new(ctx)); // Create a set that contains all registered data sources and // attach it to uid as a dynamic field to minimize the @@ -124,16 +128,28 @@ module pyth::state { data_source::add(&mut uid, vector::pop_back(&mut sources)); }; + let consumed_vaas = consumed_vaas::new(ctx); + + let required_version = required_version::new(control::version(), ctx); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + required_version::add(&mut required_version); + // Share state so that is a shared Sui object. transfer::share_object( State { id: uid, upgrade_cap, governance_data_source, - last_executed_governance_sequence: 0, stale_price_threshold, base_update_fee, - required_version: required_version::new(control::version(), ctx) + consumed_vaas, + fee_collector: fee_collector::new(base_update_fee), + required_version } ); } @@ -237,7 +253,6 @@ module pyth::state { *field::borrow_mut(&mut self.id, MigrationControl {}) = false; } - // Accessors public fun get_stale_price_threshold_secs(s: &State): u64 { s.stale_price_threshold @@ -255,15 +270,35 @@ module pyth::state { s.governance_data_source == source } - public fun get_last_executed_governance_sequence(s: &State): u64 { - s.last_executed_governance_sequence - } - public fun price_feed_object_exists(s: &State, p: PriceIdentifier): bool { price_info::contains(&s.id, p) } - // Setters + // Mutators and Setters + + /// Withdraw collected fees when governance action to transfer fees to a + /// particular recipient. + /// + /// See `pyth::transfer_fee` for more info. + public(friend) fun withdraw_fee( + self: &mut State, + amount: u64 + ): Balance { + fee_collector::withdraw_balance(&mut self.fee_collector, amount) + } + + public(friend) fun deposit_fee(self: &mut State, fee: Balance) { + fee_collector::deposit_balance(&mut self.fee_collector, fee); + } + + public(friend) fun set_fee_collector_fee(self: &mut State, amount: u64) { + fee_collector::change_fee(&mut self.fee_collector, amount); + } + + public(friend) fun consume_vaa(state: &mut State, vaa_digest: Bytes32){ + consumed_vaas::consume(&mut state.consumed_vaas, vaa_digest); + } + public(friend) fun set_data_sources(s: &mut State, new_sources: vector) { // Empty the existing table of data sources registered in state. data_source::empty(&mut s.id); @@ -277,10 +312,6 @@ module pyth::state { price_info::add(&mut s.id, price_identifier, id); } - public(friend) fun set_last_executed_governance_sequence(s: &mut State, sequence: u64) { - s.last_executed_governance_sequence = sequence; - } - public(friend) fun set_governance_data_source(s: &mut State, source: DataSource) { s. governance_data_source = source; } diff --git a/target_chains/sui/contracts/sources/version_control.move b/target_chains/sui/contracts/sources/version_control.move index 974c7b8f05..f36f482889 100644 --- a/target_chains/sui/contracts/sources/version_control.move +++ b/target_chains/sui/contracts/sources/version_control.move @@ -18,29 +18,28 @@ module pyth::version_control { /// a new implementation is built for a contract upgrade. const CURRENT_BUILD_VERSION: u64 = 1; - /// Key used to check minimum version requirement for `emitter` module. - struct Emitter {} + /// Key used to check minimum version requirement for `set_data_sources` + struct SetDataSources {} - /// Key used to check minimum version requirement for `governance_message` - /// module. - struct GovernanceMessage {} + /// Key used to check minimum version requirement for `set_governance_data_source` + struct SetGovernanceDataSource {} - /// Key used to check minimum version requirement for `publish_module` - /// module. - struct PublishMessage {} + /// Key used to check minimum version requirement for `set_stale_price_threshold` + struct SetStalePriceThreshold {} - /// Key used to check minimum version requirement for `set_fee` module. - struct SetFee {} + /// Key used to check minimum version requirement for `set_update_fee` + struct SetUpdateFee {} - /// Key used to check minimum version requirement for `transfer_fee` module. + /// Key used to check minimum version requirement for `transfer_fee` struct TransferFee {} - /// Key used to check minimum version requirement for `update_guardian_set` - /// module. - struct UpdateGuardianSet {} + /// Key used to check minimum version requirement for `update_price_feeds` + struct UpdatePriceFeeds {} - /// Key used to check minimum version requirement for `vaa` module. - struct Vaa {} + /// Key used to check minimum version requirement for `create_price_feeds` + struct CreatePriceFeeds {} + + //======================================================================= /// Return const value `CURRENT_BUILD_VERSION` for this particular build. /// This value is used to determine whether this implementation meets From b893a066afe5f6d72aee7e21f61efd1e8b550222 Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 11 Apr 2023 22:39:49 +0000 Subject: [PATCH 26/32] update clock stuff --- target_chains/sui/contracts/Move.toml | 2 +- .../sources/batch_price_attestation.move | 16 +++---- target_chains/sui/contracts/sources/pyth.move | 44 ++++++++----------- 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index 82e23e229d..7b0e5f05d2 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -5,7 +5,7 @@ version = "0.0.1" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" subdir = "crates/sui-framework/packages/sui-framework" -rev = "ddfc3fa0768a38286787319603a5458a9ff91cc1" +rev = "a63f425b9999c7fdfe483598720a9effc0acdc9e" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" diff --git a/target_chains/sui/contracts/sources/batch_price_attestation.move b/target_chains/sui/contracts/sources/batch_price_attestation.move index fd4ada51da..4ca84c595b 100644 --- a/target_chains/sui/contracts/sources/batch_price_attestation.move +++ b/target_chains/sui/contracts/sources/batch_price_attestation.move @@ -163,27 +163,23 @@ module pyth::batch_price_attestation { #[test] #[expected_failure] fun test_deserialize_batch_price_attestation_invalid_magic() { - use sui::test_scenario::{Self, take_shared, return_shared, ctx}; + use sui::test_scenario::{Self, ctx}; let test = test_scenario::begin(@0x1234); - clock::create_for_testing(ctx(&mut test)); - test_scenario::next_tx(&mut test, @0x1234); - let test_clock = take_shared(&test); - + let test_clock = clock::create_for_testing(ctx(&mut test)); // A batch price attestation with a magic number of 0x50325749 let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; let _ = destroy(deserialize(bytes, &test_clock)); - return_shared(test_clock); + clock::destroy_for_testing(test_clock); test_scenario::end(test); } #[test] fun test_deserialize_batch_price_attestation() { - use sui::test_scenario::{Self, take_shared, return_shared, ctx}; + use sui::test_scenario::{Self, ctx}; // Set the arrival time let test = test_scenario::begin(@0x1234); - clock::create_for_testing(ctx(&mut test)); + let test_clock = clock::create_for_testing(ctx(&mut test)); test_scenario::next_tx(&mut test, @0x1234); - let test_clock = take_shared(&test); let arrival_time_in_seconds = clock::timestamp_ms(&test_clock) / 1000; // let arrival_time = tx_context::epoch(ctx(&mut test)); @@ -244,7 +240,7 @@ module pyth::batch_price_attestation { assert!(&expected == &deserialized, 1); destroy(expected); destroy(deserialized); - return_shared(test_clock); + clock::destroy_for_testing(test_clock); test_scenario::end(test); } } diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index fefe00f3e1..52bd8fc73d 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -393,10 +393,10 @@ module pyth::pyth_tests{ use sui::sui::SUI; use sui::coin::{Self, Coin}; - use sui::clock::{Self, Clock}; use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared}; use sui::package::Self; use sui::object::{Self, ID}; + use sui::clock::{Self, Clock}; use pyth::state::{Self, State as PythState}; use pyth::price_identifier::{Self}; @@ -433,7 +433,7 @@ module pyth::pyth_tests{ data_sources: vector, base_update_fee: u64, to_mint: u64 - ): (Scenario, Coin) { + ): (Scenario, Coin, Clock) { let scenario = test_scenario::begin(DEPLOYER); @@ -477,9 +477,6 @@ module pyth::pyth_tests{ test_scenario::ctx(&mut scenario) ); - // Create and share a global clock object for timekeeping. - clock::create_for_testing(ctx(&mut scenario)); - // Initialize Pyth state. let pyth_upgrade_cap= package::test_publish( @@ -505,7 +502,8 @@ module pyth::pyth_tests{ ); let coins = coin::mint_for_testing(to_mint, ctx(&mut scenario)); - (scenario, coins) + let clock = clock::create_for_testing(ctx(&mut scenario)); + (scenario, coins, clock) } #[test_only] @@ -578,7 +576,7 @@ module pyth::pyth_tests{ #[test] fun test_get_update_fee() { let single_update_fee = 50; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], single_update_fee, 0); + let (scenario, test_coins, _clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], single_update_fee, 0); test_scenario::next_tx(&mut scenario, DEPLOYER, ); let pyth_state = take_shared(&scenario); // Pass in a single VAA @@ -597,17 +595,17 @@ module pyth::pyth_tests{ return_shared(pyth_state); coin::burn_for_testing(test_coins); + clock::destroy_for_testing(_clock); test_scenario::end(scenario); } #[test] #[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)] fun test_create_price_feeds_corrupt_vaa() { - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); // Pass in a corrupt VAA, which should fail deseriaizing let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; @@ -623,7 +621,7 @@ module pyth::pyth_tests{ return_shared(pyth_state); return_shared(worm_state); - return_shared(clock); + clock::destroy_for_testing(clock); coin::burn_for_testing(test_coins); test_scenario::end(scenario); } @@ -641,12 +639,11 @@ module pyth::pyth_tests{ ) ]; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -658,7 +655,7 @@ module pyth::pyth_tests{ return_shared(pyth_state); return_shared(worm_state); - return_shared(clock); + clock::destroy_for_testing(clock); coin::burn_for_testing(test_coins); test_scenario::end(scenario); } @@ -682,12 +679,11 @@ module pyth::pyth_tests{ let base_update_fee = 50; let coins_to_mint = 5000; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -737,7 +733,7 @@ module pyth::pyth_tests{ return_shared(price_info_object_3); return_shared(price_info_object_4); - return_shared(clock); + clock::destroy_for_testing(clock); test_scenario::end(scenario); } @@ -748,12 +744,11 @@ module pyth::pyth_tests{ let base_update_fee = 50; let coins_to_mint = 5000; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -812,12 +807,11 @@ module pyth::pyth_tests{ let base_update_fee = 50; let coins_to_mint = 5; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -856,12 +850,11 @@ module pyth::pyth_tests{ let base_update_fee = 50; let coins_to_mint = 5000; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -905,7 +898,7 @@ module pyth::pyth_tests{ return_shared(price_info_object_4); coin::burn_for_testing(test_coins); - return_shared(clock); + clock::destroy_for_testing(clock); test_scenario::end(scenario); } @@ -915,12 +908,11 @@ module pyth::pyth_tests{ let base_update_fee = 50; let coins_to_mint = 5000; - let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); + let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); test_scenario::next_tx(&mut scenario, DEPLOYER); let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); - let clock = take_shared(&scenario); pyth::create_price_feeds( &mut worm_state, @@ -1031,7 +1023,7 @@ module pyth::pyth_tests{ return_shared(price_info_object_4); coin::burn_for_testing(test_coins); - return_shared(clock); + clock::destroy_for_testing(clock); test_scenario::end(scenario); } } From 466b1223bcca945b1c9ae969c8c030f940cf2441 Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 11 Apr 2023 22:42:51 +0000 Subject: [PATCH 27/32] edits --- target_chains/sui/contracts/sources/pyth.move | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 52bd8fc73d..942654cb45 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -964,7 +964,6 @@ module pyth::pyth_tests{ vector::destroy_empty(price_info_object_vec); - // Confirm that the current price and ema price didn't change let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); let current_price_feed = price_info::get_price_feed(¤t_price_info); let current_price = price_feed::get_price(current_price_feed); @@ -1006,7 +1005,7 @@ module pyth::pyth_tests{ vector::destroy_empty(price_info_object_vec); - // Confirm that the Pyth cached price got updated to fresh_price + // Confirm that the Pyth cached price got updated to fresh_price. let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); let current_price_feed = price_info::get_price_feed(¤t_price_info); let current_price = price_feed::get_price(current_price_feed); From caea90ed9ebe73878af3136da4d1aa4b77ea732b Mon Sep 17 00:00:00 2001 From: optke3 Date: Tue, 11 Apr 2023 23:07:38 +0000 Subject: [PATCH 28/32] use clone of sui/integration_v2 for stability --- target_chains/sui/contracts/Move.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/target_chains/sui/contracts/Move.toml b/target_chains/sui/contracts/Move.toml index 7b0e5f05d2..692360abac 100644 --- a/target_chains/sui/contracts/Move.toml +++ b/target_chains/sui/contracts/Move.toml @@ -10,7 +10,7 @@ rev = "a63f425b9999c7fdfe483598720a9effc0acdc9e" [dependencies.Wormhole] git = "https://github.com/wormhole-foundation/wormhole.git" subdir = "sui/wormhole" -rev = "sui/integration_v2" +rev = "sui/integration_v2_stable" [addresses] pyth = "0x250" From a70e387e9b749d89c75cd1f6eb9e9f3a00473e56 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 12 Apr 2023 03:42:20 +0000 Subject: [PATCH 29/32] make contract_upgrade::execute a public(friend) fun, remove clock arg --- .../sui/contracts/sources/governance/contract_upgrade.move | 4 +--- .../sui/contracts/sources/governance/governance.move | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move index 6b9453861f..8d6fc4b931 100644 --- a/target_chains/sui/contracts/sources/governance/contract_upgrade.move +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -12,7 +12,6 @@ /// 3. Upgrade. /// 4. Commit upgrade. module pyth::contract_upgrade { - use sui::clock::{Clock}; use sui::event::{Self}; use sui::object::{Self, ID}; use sui::package::{UpgradeReceipt, UpgradeTicket}; @@ -47,10 +46,9 @@ module pyth::contract_upgrade { /// /// NOTE: This method is guarded by a minimum build version check. This /// method could break backward compatibility on an upgrade. - public fun execute( + public(friend) fun execute( pyth_state: &mut State, payload: vector, - _the_clock: &Clock ): UpgradeTicket { // Proceed with processing new implementation version. handle_upgrade_contract(pyth_state, payload) diff --git a/target_chains/sui/contracts/sources/governance/governance.move b/target_chains/sui/contracts/sources/governance/governance.move index 1198e70fdc..c2104282a7 100644 --- a/target_chains/sui/contracts/sources/governance/governance.move +++ b/target_chains/sui/contracts/sources/governance/governance.move @@ -42,7 +42,7 @@ module pyth::governance { E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE); assert!(governance_instruction::get_target_chain_id(&instruction) != 0, E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO); - contract_upgrade::execute(pyth_state, governance_instruction::destroy(instruction), clock) + contract_upgrade::execute(pyth_state, governance_instruction::destroy(instruction)) } /// Execute a governance instruction. From 4d04054ab8b602f02951b289b05005c0a482eab9 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 12 Apr 2023 03:47:52 +0000 Subject: [PATCH 30/32] E_INCORRECT_IDENTIFIER_LENGTH --- .../sources/governance/contract_upgrade.move | 20 +++++++++---------- .../contracts/sources/price_identifier.move | 5 ++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/target_chains/sui/contracts/sources/governance/contract_upgrade.move b/target_chains/sui/contracts/sources/governance/contract_upgrade.move index 8d6fc4b931..9c2f26796d 100644 --- a/target_chains/sui/contracts/sources/governance/contract_upgrade.move +++ b/target_chains/sui/contracts/sources/governance/contract_upgrade.move @@ -54,6 +54,16 @@ module pyth::contract_upgrade { handle_upgrade_contract(pyth_state, payload) } + fun handle_upgrade_contract( + pyth_state: &mut State, + payload: vector + ): UpgradeTicket { + + let UpgradeContract { digest } = deserialize(payload); + + state::authorize_upgrade(pyth_state, digest) + } + /// Finalize the upgrade that ran to produce the given `receipt`. This /// method invokes `state::commit_upgrade` which interacts with /// `sui::package`. @@ -72,16 +82,6 @@ module pyth::contract_upgrade { ); } - fun handle_upgrade_contract( - pyth_state: &mut State, - payload: vector - ): UpgradeTicket { - - let UpgradeContract { digest } = deserialize(payload); - - state::authorize_upgrade(pyth_state, digest) - } - fun deserialize(payload: vector): UpgradeContract { let cur = cursor::new(payload); diff --git a/target_chains/sui/contracts/sources/price_identifier.move b/target_chains/sui/contracts/sources/price_identifier.move index 75df3a88a0..5191c4c6b4 100644 --- a/target_chains/sui/contracts/sources/price_identifier.move +++ b/target_chains/sui/contracts/sources/price_identifier.move @@ -1,16 +1,15 @@ module pyth::price_identifier { use std::vector; - //use pyth::error; const IDENTIFIER_BYTES_LENGTH: u64 = 32; + const E_INCORRECT_IDENTIFIER_LENGTH: u64 = 0; struct PriceIdentifier has copy, drop, store { bytes: vector, } public fun from_byte_vec(bytes: vector): PriceIdentifier { - assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, 0); //error::incorrect_identifier_length() - + assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, E_INCORRECT_IDENTIFIER_LENGTH); PriceIdentifier { bytes: bytes } From 94feb86ea5cf301f69fff4689ce2fc7697733c11 Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 12 Apr 2023 03:57:35 +0000 Subject: [PATCH 31/32] comment and edit --- target_chains/sui/contracts/sources/migrate.move | 2 +- target_chains/sui/contracts/sources/version_control.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/target_chains/sui/contracts/sources/migrate.move b/target_chains/sui/contracts/sources/migrate.move index 55b519ffe5..ffd85af5c9 100644 --- a/target_chains/sui/contracts/sources/migrate.move +++ b/target_chains/sui/contracts/sources/migrate.move @@ -14,7 +14,7 @@ module pyth::migrate { use pyth::state::{Self, State}; // This import is only used when `state::require_current_version` is used. - // use pytg::version_control::{Self as control}; + // use pyth::version_control::{Self as control}; /// Upgrade procedure is not complete (most likely due to an upgrade not /// being initialized since upgrades can only be performed via programmable diff --git a/target_chains/sui/contracts/sources/version_control.move b/target_chains/sui/contracts/sources/version_control.move index f36f482889..232e8d3827 100644 --- a/target_chains/sui/contracts/sources/version_control.move +++ b/target_chains/sui/contracts/sources/version_control.move @@ -12,7 +12,7 @@ /// /// See `pyth::required_version` and `pyth::state` for more info. module pyth::version_control { - /// This value tracks the current version of the Wormhole version. We are + /// This value tracks the current Pyth contract version. We are /// placing this constant value at the top, which goes against Move style /// guides so that we bring special attention to changing this value when /// a new implementation is built for a contract upgrade. From 178f05c746c402879e3b816cf49af0224573346d Mon Sep 17 00:00:00 2001 From: optke3 Date: Wed, 12 Apr 2023 17:20:58 +0000 Subject: [PATCH 32/32] add a single comment --- target_chains/sui/contracts/sources/pyth.move | 1 + 1 file changed, 1 insertion(+) diff --git a/target_chains/sui/contracts/sources/pyth.move b/target_chains/sui/contracts/sources/pyth.move index 942654cb45..a4ace1939f 100644 --- a/target_chains/sui/contracts/sources/pyth.move +++ b/target_chains/sui/contracts/sources/pyth.move @@ -856,6 +856,7 @@ module pyth::pyth_tests{ let pyth_state = take_shared(&scenario); let worm_state = take_shared(&scenario); + // Update cache is called by create_price_feeds. pyth::create_price_feeds( &mut worm_state, &mut pyth_state,